@assistant-ui/react-generative-ui 0.0.6 → 0.0.8
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/JSONGenerativeUI.client.d.ts +4 -2
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +9 -2
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -1
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +8 -1
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.js.map +1 -1
- package/dist/actionRegistry.d.ts +32 -0
- package/dist/actionRegistry.d.ts.map +1 -0
- package/dist/actionRegistry.js +41 -0
- package/dist/actionRegistry.js.map +1 -0
- package/dist/buildPresentParameters.d.ts +5 -5
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +20 -6
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +0 -1
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +15 -10
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/generativeUIToJSX.js +47 -17
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -2
- package/dist/index.server.d.ts +13 -0
- package/dist/index.server.js +10 -0
- package/dist/index.shared.d.ts +11 -0
- package/dist/index.shared.js +9 -0
- package/dist/ir.d.ts +115 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +175 -0
- package/dist/ir.js.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +5 -5
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +18 -45
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +111 -0
- package/dist/slack/constants.d.ts.map +1 -0
- package/dist/slack/constants.js +168 -0
- package/dist/slack/constants.js.map +1 -0
- package/dist/slack/decodeBlockAction.d.ts +11 -0
- package/dist/slack/decodeBlockAction.d.ts.map +1 -0
- package/dist/slack/decodeBlockAction.js +40 -0
- package/dist/slack/decodeBlockAction.js.map +1 -0
- package/dist/slack/fromSlackBlocks.d.ts +30 -0
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
- package/dist/slack/fromSlackBlocks.js +447 -0
- package/dist/slack/fromSlackBlocks.js.map +1 -0
- package/dist/slack/toSlackBlocks.d.ts +7 -0
- package/dist/slack/toSlackBlocks.d.ts.map +1 -0
- package/dist/slack/toSlackBlocks.js +649 -0
- package/dist/slack/toSlackBlocks.js.map +1 -0
- package/dist/slack/types.d.ts +178 -0
- package/dist/slack/types.d.ts.map +1 -0
- package/dist/slack/types.js +0 -0
- package/dist/slack.d.ts +5 -0
- package/dist/slack.js +4 -0
- package/dist/teams/boundSpec.d.ts +20 -0
- package/dist/teams/boundSpec.d.ts.map +1 -0
- package/dist/teams/boundSpec.js +67 -0
- package/dist/teams/boundSpec.js.map +1 -0
- package/dist/teams/constants.d.ts +57 -0
- package/dist/teams/constants.d.ts.map +1 -0
- package/dist/teams/constants.js +92 -0
- package/dist/teams/constants.js.map +1 -0
- package/dist/teams/decodeSubmitData.d.ts +25 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -0
- package/dist/teams/decodeSubmitData.js +44 -0
- package/dist/teams/decodeSubmitData.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +39 -0
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
- package/dist/teams/toAdaptiveCard.js +496 -0
- package/dist/teams/toAdaptiveCard.js.map +1 -0
- package/dist/teams/toTeamsAttachments.d.ts +13 -0
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
- package/dist/teams/toTeamsAttachments.js +95 -0
- package/dist/teams/toTeamsAttachments.js.map +1 -0
- package/dist/teams/types.d.ts +196 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +0 -0
- package/dist/teams.d.ts +5 -0
- package/dist/teams.js +4 -0
- package/dist/types.d.ts +41 -34
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +28 -0
- package/dist/vocabulary/alert.d.ts.map +1 -0
- package/dist/vocabulary/alert.js +58 -0
- package/dist/vocabulary/alert.js.map +1 -0
- package/dist/vocabulary/collectFormValues.d.ts +23 -0
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
- package/dist/vocabulary/collectFormValues.js +32 -0
- package/dist/vocabulary/collectFormValues.js.map +1 -0
- package/dist/vocabulary/data.d.ts +52 -0
- package/dist/vocabulary/data.d.ts.map +1 -0
- package/dist/vocabulary/data.js +294 -0
- package/dist/vocabulary/data.js.map +1 -0
- package/dist/vocabulary/dispatch.d.ts +11 -0
- package/dist/vocabulary/dispatch.d.ts.map +1 -0
- package/dist/vocabulary/dispatch.js +21 -0
- package/dist/vocabulary/dispatch.js.map +1 -0
- package/dist/vocabulary/fact.d.ts +15 -0
- package/dist/vocabulary/fact.d.ts.map +1 -0
- package/dist/vocabulary/fact.js +24 -0
- package/dist/vocabulary/fact.js.map +1 -0
- package/dist/vocabulary/form.d.ts +14 -0
- package/dist/vocabulary/form.d.ts.map +1 -0
- package/dist/vocabulary/form.js +23 -0
- package/dist/vocabulary/form.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +44 -0
- package/dist/vocabulary/icon.d.ts.map +1 -0
- package/dist/vocabulary/icon.js +272 -0
- package/dist/vocabulary/icon.js.map +1 -0
- package/dist/vocabulary/index.d.ts +15 -0
- package/dist/vocabulary/index.d.ts.map +1 -0
- package/dist/vocabulary/index.js +36 -0
- package/dist/vocabulary/index.js.map +1 -0
- package/dist/vocabulary/interactive.d.ts +93 -0
- package/dist/vocabulary/interactive.d.ts.map +1 -0
- package/dist/vocabulary/interactive.js +172 -0
- package/dist/vocabulary/interactive.js.map +1 -0
- package/dist/vocabulary/layout.d.ts +82 -0
- package/dist/vocabulary/layout.d.ts.map +1 -0
- package/dist/vocabulary/layout.js +136 -0
- package/dist/vocabulary/layout.js.map +1 -0
- package/dist/vocabulary/list.d.ts +17 -0
- package/dist/vocabulary/list.d.ts.map +1 -0
- package/dist/vocabulary/list.js +48 -0
- package/dist/vocabulary/list.js.map +1 -0
- package/dist/vocabulary/media.d.ts +28 -0
- package/dist/vocabulary/media.d.ts.map +1 -0
- package/dist/vocabulary/media.js +42 -0
- package/dist/vocabulary/media.js.map +1 -0
- package/dist/vocabulary/text.d.ts +60 -0
- package/dist/vocabulary/text.d.ts.map +1 -0
- package/dist/vocabulary/text.js +48 -0
- package/dist/vocabulary/text.js.map +1 -0
- package/package.json +21 -13
- package/src/JSONGenerativeUI.client.tsx +10 -2
- package/src/JSONGenerativeUI.shared.ts +8 -0
- package/src/actionRegistry.test.ts +90 -0
- package/src/actionRegistry.ts +88 -0
- package/src/buildPresentParameters.ts +38 -16
- package/src/generativeUIToJSX.test.ts +186 -0
- package/src/generativeUIToJSX.ts +112 -21
- package/src/index.server.ts +2 -0
- package/src/index.shared.ts +61 -0
- package/src/index.ts +2 -26
- package/src/ir.test.ts +203 -0
- package/src/ir.ts +279 -0
- package/src/renderGenerativeUI.test.tsx +69 -2
- package/src/renderGenerativeUI.tsx +49 -82
- package/src/slack/constants.ts +217 -0
- package/src/slack/decodeBlockAction.test.ts +135 -0
- package/src/slack/decodeBlockAction.ts +66 -0
- package/src/slack/fromSlackBlocks.test.ts +1145 -0
- package/src/slack/fromSlackBlocks.ts +709 -0
- package/src/slack/toSlackBlocks.test.ts +1936 -0
- package/src/slack/toSlackBlocks.ts +1319 -0
- package/src/slack/types.ts +230 -0
- package/src/slack.ts +37 -0
- package/src/teams/boundSpec.ts +102 -0
- package/src/teams/constants.ts +127 -0
- package/src/teams/decodeSubmitData.test.ts +148 -0
- package/src/teams/decodeSubmitData.ts +57 -0
- package/src/teams/toAdaptiveCard.test.ts +1410 -0
- package/src/teams/toAdaptiveCard.ts +762 -0
- package/src/teams/toTeamsAttachments.test.ts +29 -0
- package/src/teams/toTeamsAttachments.ts +137 -0
- package/src/teams/types.ts +237 -0
- package/src/teams.ts +34 -0
- package/src/types.ts +57 -42
- package/src/vocabulary/alert.test.tsx +136 -0
- package/src/vocabulary/alert.tsx +66 -0
- package/src/vocabulary/collectFormValues.test.ts +127 -0
- package/src/vocabulary/collectFormValues.ts +58 -0
- package/src/vocabulary/data.test.tsx +567 -0
- package/src/vocabulary/data.tsx +494 -0
- package/src/vocabulary/dispatch.ts +22 -0
- package/src/vocabulary/fact.test.tsx +15 -0
- package/src/vocabulary/fact.tsx +22 -0
- package/src/vocabulary/form.test.tsx +112 -0
- package/src/vocabulary/form.tsx +34 -0
- package/src/vocabulary/icon.test.tsx +64 -0
- package/src/vocabulary/icon.tsx +174 -0
- package/src/vocabulary/index.test.ts +63 -0
- package/src/vocabulary/index.ts +33 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
- package/src/vocabulary/interactive.test.tsx +248 -0
- package/src/vocabulary/interactive.tsx +269 -0
- package/src/vocabulary/layout.test.tsx +289 -0
- package/src/vocabulary/layout.tsx +229 -0
- package/src/vocabulary/list.test.tsx +195 -0
- package/src/vocabulary/list.tsx +48 -0
- package/src/vocabulary/media.test.tsx +85 -0
- package/src/vocabulary/media.tsx +56 -0
- package/src/vocabulary/text.test.tsx +73 -0
- package/src/vocabulary/text.tsx +59 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toTeamsAttachments } from "./toTeamsAttachments";
|
|
3
|
+
import { PAYLOAD_SOFT_CAP } from "./constants";
|
|
4
|
+
|
|
5
|
+
describe("toTeamsAttachments payload budget", () => {
|
|
6
|
+
it("warns on the aggregate carousel size even when no single card exceeds the soft cap", () => {
|
|
7
|
+
const value = "x".repeat(45000);
|
|
8
|
+
const { attachments, warnings } = toTeamsAttachments({
|
|
9
|
+
$type: "Carousel",
|
|
10
|
+
children: [
|
|
11
|
+
{ $type: "Card", title: "A", children: { $type: "Markdown", value } },
|
|
12
|
+
{ $type: "Card", title: "B", children: { $type: "Markdown", value } },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
for (const attachment of attachments) {
|
|
16
|
+
expect(JSON.stringify(attachment.content).length).toBeLessThanOrEqual(
|
|
17
|
+
PAYLOAD_SOFT_CAP,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
expect(
|
|
21
|
+
warnings.some(
|
|
22
|
+
(warning) => warning.code === "clamped" && warning.component === "Root",
|
|
23
|
+
),
|
|
24
|
+
).toBe(false);
|
|
25
|
+
expect(warnings).toContainEqual(
|
|
26
|
+
expect.objectContaining({ code: "clamped", component: "Carousel" }),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeSpec,
|
|
3
|
+
type NormalizedUIElement,
|
|
4
|
+
type NormalizedUINode,
|
|
5
|
+
} from "../ir";
|
|
6
|
+
import { boundSpec } from "./boundSpec";
|
|
7
|
+
import {
|
|
8
|
+
CAROUSEL_ATTACHMENT_CAP,
|
|
9
|
+
PAYLOAD_SOFT_CAP,
|
|
10
|
+
buildAttachment,
|
|
11
|
+
clampReasonDetail,
|
|
12
|
+
utf8ByteLength,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
import {
|
|
15
|
+
convertElement,
|
|
16
|
+
convertRootToCard,
|
|
17
|
+
type ConversionContext,
|
|
18
|
+
} from "./toAdaptiveCard";
|
|
19
|
+
import type {
|
|
20
|
+
TeamsAttachmentsResult,
|
|
21
|
+
TeamsCardAttachment,
|
|
22
|
+
TeamsConversionWarning,
|
|
23
|
+
ToAdaptiveCardOptions,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
27
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
|
|
30
|
+
isRecord(node);
|
|
31
|
+
|
|
32
|
+
const normalizedList = (
|
|
33
|
+
node: NormalizedUINode | undefined,
|
|
34
|
+
): NormalizedUINode[] => {
|
|
35
|
+
if (node === undefined || node === null) return [];
|
|
36
|
+
if (!Array.isArray(node)) return [node];
|
|
37
|
+
return node.flatMap((child) => normalizedList(child));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const clampArray = <T>(
|
|
41
|
+
value: readonly T[],
|
|
42
|
+
cap: number,
|
|
43
|
+
): { readonly items: T[]; readonly truncated: boolean } => ({
|
|
44
|
+
items: value.slice(0, cap),
|
|
45
|
+
truncated: value.length > cap,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recognizes a root that is a single `Carousel` element, returning its `Card`
|
|
50
|
+
* children. Every child is routed through {@link convertElement} first (its
|
|
51
|
+
* result is discarded) so an unknown or unsupported child still produces its
|
|
52
|
+
* usual "dropped" warning instead of being filtered out silently; non-card
|
|
53
|
+
* children are then dropped. `undefined` means the root is not exclusively a
|
|
54
|
+
* root-level carousel, so the caller falls back to converting the whole tree
|
|
55
|
+
* as one card.
|
|
56
|
+
*/
|
|
57
|
+
function rootCarouselCards(
|
|
58
|
+
root: NormalizedUINode | readonly NormalizedUINode[],
|
|
59
|
+
context: ConversionContext,
|
|
60
|
+
): NormalizedUIElement[] | undefined {
|
|
61
|
+
const element = Array.isArray(root) && root.length === 1 ? root[0] : root;
|
|
62
|
+
if (!isElement(element) || element.type !== "Carousel") return undefined;
|
|
63
|
+
const cards: NormalizedUIElement[] = [];
|
|
64
|
+
for (const child of normalizedList(element.children)) {
|
|
65
|
+
if (!isElement(child)) continue;
|
|
66
|
+
if (child.type === "Card") {
|
|
67
|
+
cards.push(child);
|
|
68
|
+
} else {
|
|
69
|
+
convertElement(child, context, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return cards;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Converts a generative-ui tree into Teams bot-framework message attachments.
|
|
77
|
+
* A root-level `Carousel` becomes one attachment per `Card` child (clamped to
|
|
78
|
+
* {@link CAROUSEL_ATTACHMENT_CAP}) with `attachmentLayout: "carousel"`;
|
|
79
|
+
* anything else converts through {@link toAdaptiveCard}'s engine into a
|
|
80
|
+
* single attachment. Never throws.
|
|
81
|
+
*/
|
|
82
|
+
export function toTeamsAttachments(
|
|
83
|
+
node: unknown,
|
|
84
|
+
_options?: ToAdaptiveCardOptions,
|
|
85
|
+
): TeamsAttachmentsResult {
|
|
86
|
+
const warnings: TeamsConversionWarning[] = [];
|
|
87
|
+
const context: ConversionContext = { warnings, usedInputIds: new Set() };
|
|
88
|
+
try {
|
|
89
|
+
const bounded = boundSpec(node, (reason) =>
|
|
90
|
+
warnings.push({
|
|
91
|
+
code: "clamped",
|
|
92
|
+
component: "Root",
|
|
93
|
+
detail: clampReasonDetail(reason),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
const { root } = normalizeSpec(bounded as never);
|
|
97
|
+
const carouselCards = rootCarouselCards(root, context);
|
|
98
|
+
if (carouselCards !== undefined) {
|
|
99
|
+
const { items, truncated } = clampArray(
|
|
100
|
+
carouselCards,
|
|
101
|
+
CAROUSEL_ATTACHMENT_CAP,
|
|
102
|
+
);
|
|
103
|
+
if (truncated) {
|
|
104
|
+
warnings.push({
|
|
105
|
+
code: "clamped",
|
|
106
|
+
component: "Carousel",
|
|
107
|
+
detail: `cards were clamped to ${CAROUSEL_ATTACHMENT_CAP} entries.`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const attachments: TeamsCardAttachment[] = items.map((child) =>
|
|
111
|
+
buildAttachment(convertRootToCard(child, context)),
|
|
112
|
+
);
|
|
113
|
+
const size = utf8ByteLength(JSON.stringify(attachments));
|
|
114
|
+
if (size > PAYLOAD_SOFT_CAP) {
|
|
115
|
+
warnings.push({
|
|
116
|
+
code: "clamped",
|
|
117
|
+
component: "Carousel",
|
|
118
|
+
detail: `the carousel attachments total ${size} bytes, exceeding Teams' 100 KB bot message limit.`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { attachments, attachmentLayout: "carousel", warnings };
|
|
122
|
+
}
|
|
123
|
+
const attachment = buildAttachment(convertRootToCard(root, context));
|
|
124
|
+
return { attachments: [attachment], warnings };
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
attachments: [],
|
|
128
|
+
warnings: [
|
|
129
|
+
{
|
|
130
|
+
code: "dropped",
|
|
131
|
+
component: "Root",
|
|
132
|
+
detail: "Input could not be converted.",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** A TextBlock size, from Adaptive Card's closed size enum. */
|
|
2
|
+
export type TeamsTextSize = "small" | "medium" | "large" | "extraLarge";
|
|
3
|
+
|
|
4
|
+
/** A Container's style, driving its accent color in Teams' renderer. */
|
|
5
|
+
export type TeamsContainerStyle =
|
|
6
|
+
| "default"
|
|
7
|
+
| "emphasis"
|
|
8
|
+
| "good"
|
|
9
|
+
| "attention"
|
|
10
|
+
| "warning"
|
|
11
|
+
| "accent";
|
|
12
|
+
|
|
13
|
+
/** A run of wrapped text. Every TextBlock we emit sets `wrap: true`. */
|
|
14
|
+
export interface TeamsTextBlock {
|
|
15
|
+
readonly type: "TextBlock";
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly wrap: true;
|
|
18
|
+
readonly size?: TeamsTextSize;
|
|
19
|
+
readonly weight?: "bolder";
|
|
20
|
+
readonly style?: "heading";
|
|
21
|
+
readonly isSubtle?: true;
|
|
22
|
+
readonly separator?: true;
|
|
23
|
+
readonly spacing?: "large";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** An image element. */
|
|
27
|
+
export interface TeamsImage {
|
|
28
|
+
readonly type: "Image";
|
|
29
|
+
readonly url: string;
|
|
30
|
+
readonly altText?: string;
|
|
31
|
+
readonly size?: "small" | "medium" | "large";
|
|
32
|
+
readonly separator?: true;
|
|
33
|
+
readonly spacing?: "large";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One label/value entry inside a {@link TeamsFactSet}. */
|
|
37
|
+
export interface TeamsFact {
|
|
38
|
+
readonly title: string;
|
|
39
|
+
readonly value: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A set of label/value facts, rendered as an aligned two-column list. */
|
|
43
|
+
export interface TeamsFactSet {
|
|
44
|
+
readonly type: "FactSet";
|
|
45
|
+
readonly facts: readonly TeamsFact[];
|
|
46
|
+
readonly separator?: true;
|
|
47
|
+
readonly spacing?: "large";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `aui` envelope nested inside every {@link TeamsSubmitAction}'s `data`. */
|
|
51
|
+
export interface TeamsSubmitData {
|
|
52
|
+
readonly aui: {
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly payload?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A submit action. `data` carries the resume payload under an `aui` key so
|
|
60
|
+
* {@link decodeSubmitData} can invert it out of the merged `activity.value`
|
|
61
|
+
* a Teams bot receives, alongside whatever other named inputs shared the card.
|
|
62
|
+
*/
|
|
63
|
+
export interface TeamsSubmitAction {
|
|
64
|
+
readonly type: "Action.Submit";
|
|
65
|
+
readonly title: string;
|
|
66
|
+
readonly data: TeamsSubmitData;
|
|
67
|
+
readonly mode?: "secondary";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The action shape accepted at the card's top-level `actions` field. */
|
|
71
|
+
export type TeamsCardAction = TeamsSubmitAction;
|
|
72
|
+
|
|
73
|
+
/** A bar of submit actions, rendered inline in the body flow. */
|
|
74
|
+
export interface TeamsActionSet {
|
|
75
|
+
readonly type: "ActionSet";
|
|
76
|
+
readonly actions: readonly TeamsSubmitAction[];
|
|
77
|
+
readonly separator?: true;
|
|
78
|
+
readonly spacing?: "large";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A bordered grouping container. */
|
|
82
|
+
export interface TeamsContainer {
|
|
83
|
+
readonly type: "Container";
|
|
84
|
+
readonly style?: TeamsContainerStyle;
|
|
85
|
+
readonly items: readonly TeamsCardElement[];
|
|
86
|
+
readonly selectAction?: TeamsSubmitAction;
|
|
87
|
+
readonly separator?: true;
|
|
88
|
+
readonly spacing?: "large";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** One column inside a {@link TeamsColumnSet}. */
|
|
92
|
+
export interface TeamsColumn {
|
|
93
|
+
readonly type: "Column";
|
|
94
|
+
readonly width: "auto";
|
|
95
|
+
readonly items: readonly TeamsCardElement[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A horizontal row of columns. */
|
|
99
|
+
export interface TeamsColumnSet {
|
|
100
|
+
readonly type: "ColumnSet";
|
|
101
|
+
readonly columns: readonly TeamsColumn[];
|
|
102
|
+
readonly separator?: true;
|
|
103
|
+
readonly spacing?: "large";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One selectable choice inside a {@link TeamsInputChoiceSet}. */
|
|
107
|
+
export interface TeamsInputChoice {
|
|
108
|
+
readonly title: string;
|
|
109
|
+
readonly value: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A dropdown (`compact`) or radio group (`expanded`) input. */
|
|
113
|
+
export interface TeamsInputChoiceSet {
|
|
114
|
+
readonly type: "Input.ChoiceSet";
|
|
115
|
+
readonly id: string;
|
|
116
|
+
readonly style: "compact" | "expanded";
|
|
117
|
+
readonly choices: readonly TeamsInputChoice[];
|
|
118
|
+
readonly placeholder?: string;
|
|
119
|
+
readonly label?: string;
|
|
120
|
+
readonly value?: string;
|
|
121
|
+
readonly separator?: true;
|
|
122
|
+
readonly spacing?: "large";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A checkbox-shaped boolean input. */
|
|
126
|
+
export interface TeamsInputToggle {
|
|
127
|
+
readonly type: "Input.Toggle";
|
|
128
|
+
readonly id: string;
|
|
129
|
+
readonly title: string;
|
|
130
|
+
readonly value: "true" | "false";
|
|
131
|
+
readonly valueOn: "true";
|
|
132
|
+
readonly valueOff: "false";
|
|
133
|
+
readonly separator?: true;
|
|
134
|
+
readonly spacing?: "large";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A single or multi-line text input. */
|
|
138
|
+
export interface TeamsInputText {
|
|
139
|
+
readonly type: "Input.Text";
|
|
140
|
+
readonly id: string;
|
|
141
|
+
readonly label?: string;
|
|
142
|
+
readonly placeholder?: string;
|
|
143
|
+
readonly isMultiline?: true;
|
|
144
|
+
readonly separator?: true;
|
|
145
|
+
readonly spacing?: "large";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A date input, restricted to `YYYY-MM-DD` values. */
|
|
149
|
+
export interface TeamsInputDate {
|
|
150
|
+
readonly type: "Input.Date";
|
|
151
|
+
readonly id: string;
|
|
152
|
+
readonly label?: string;
|
|
153
|
+
readonly value?: string;
|
|
154
|
+
readonly min?: string;
|
|
155
|
+
readonly max?: string;
|
|
156
|
+
readonly separator?: true;
|
|
157
|
+
readonly spacing?: "large";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One cell inside a {@link TeamsTableRow}. */
|
|
161
|
+
export interface TeamsTableCell {
|
|
162
|
+
readonly type: "TableCell";
|
|
163
|
+
readonly items: readonly TeamsTextBlock[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** One row inside a {@link TeamsTable}. */
|
|
167
|
+
export interface TeamsTableRow {
|
|
168
|
+
readonly type: "TableRow";
|
|
169
|
+
readonly cells: readonly TeamsTableCell[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A column width definition inside a {@link TeamsTable}. */
|
|
173
|
+
export interface TeamsTableColumnDefinition {
|
|
174
|
+
readonly width: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A native table. `rows[0]` is the header row when `firstRowAsHeaders` is true. */
|
|
178
|
+
export interface TeamsTable {
|
|
179
|
+
readonly type: "Table";
|
|
180
|
+
readonly firstRowAsHeaders: boolean;
|
|
181
|
+
readonly columns: readonly TeamsTableColumnDefinition[];
|
|
182
|
+
readonly rows: readonly TeamsTableRow[];
|
|
183
|
+
readonly separator?: true;
|
|
184
|
+
readonly spacing?: "large";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A body element emitted by {@link toAdaptiveCard}. */
|
|
188
|
+
export type TeamsCardElement =
|
|
189
|
+
| TeamsTextBlock
|
|
190
|
+
| TeamsImage
|
|
191
|
+
| TeamsFactSet
|
|
192
|
+
| TeamsActionSet
|
|
193
|
+
| TeamsContainer
|
|
194
|
+
| TeamsColumnSet
|
|
195
|
+
| TeamsInputChoiceSet
|
|
196
|
+
| TeamsInputToggle
|
|
197
|
+
| TeamsInputText
|
|
198
|
+
| TeamsInputDate
|
|
199
|
+
| TeamsTable;
|
|
200
|
+
|
|
201
|
+
/** A Microsoft Teams Adaptive Card, pinned to schema version 1.5 (the Teams desktop/web cap; mobile clients cap at 1.2). */
|
|
202
|
+
export interface TeamsAdaptiveCard {
|
|
203
|
+
readonly $schema: "http://adaptivecards.io/schemas/adaptive-card.json";
|
|
204
|
+
readonly type: "AdaptiveCard";
|
|
205
|
+
readonly version: "1.5";
|
|
206
|
+
readonly body: readonly TeamsCardElement[];
|
|
207
|
+
readonly actions: readonly TeamsCardAction[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** An Adaptive Card wrapped as a bot-framework message attachment. */
|
|
211
|
+
export interface TeamsCardAttachment {
|
|
212
|
+
readonly contentType: "application/vnd.microsoft.card.adaptive";
|
|
213
|
+
readonly content: TeamsAdaptiveCard;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A non-fatal downgrade reported during Teams conversion. */
|
|
217
|
+
export interface TeamsConversionWarning {
|
|
218
|
+
readonly code: "clamped" | "dropped" | "fallback";
|
|
219
|
+
readonly component: string;
|
|
220
|
+
readonly detail: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Options accepted by {@link toAdaptiveCard} and {@link toTeamsAttachments}. Reserved for future extension; no options are currently defined. */
|
|
224
|
+
export interface ToAdaptiveCardOptions {}
|
|
225
|
+
|
|
226
|
+
/** The card and non-fatal warnings produced by {@link toAdaptiveCard}. */
|
|
227
|
+
export interface AdaptiveCardResult {
|
|
228
|
+
readonly card: TeamsAdaptiveCard;
|
|
229
|
+
readonly warnings: TeamsConversionWarning[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The attachments and non-fatal warnings produced by {@link toTeamsAttachments}. */
|
|
233
|
+
export interface TeamsAttachmentsResult {
|
|
234
|
+
readonly attachments: TeamsCardAttachment[];
|
|
235
|
+
readonly attachmentLayout?: "carousel";
|
|
236
|
+
readonly warnings: TeamsConversionWarning[];
|
|
237
|
+
}
|
package/src/teams.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { decodeSubmitData } from "./teams/decodeSubmitData";
|
|
2
|
+
export { toAdaptiveCard } from "./teams/toAdaptiveCard";
|
|
3
|
+
export { toTeamsAttachments } from "./teams/toTeamsAttachments";
|
|
4
|
+
export type {
|
|
5
|
+
AdaptiveCardResult,
|
|
6
|
+
TeamsAdaptiveCard,
|
|
7
|
+
TeamsActionSet,
|
|
8
|
+
TeamsAttachmentsResult,
|
|
9
|
+
TeamsCardAction,
|
|
10
|
+
TeamsCardAttachment,
|
|
11
|
+
TeamsCardElement,
|
|
12
|
+
TeamsColumn,
|
|
13
|
+
TeamsColumnSet,
|
|
14
|
+
TeamsContainer,
|
|
15
|
+
TeamsContainerStyle,
|
|
16
|
+
TeamsConversionWarning,
|
|
17
|
+
TeamsFact,
|
|
18
|
+
TeamsFactSet,
|
|
19
|
+
TeamsImage,
|
|
20
|
+
TeamsInputChoice,
|
|
21
|
+
TeamsInputChoiceSet,
|
|
22
|
+
TeamsInputDate,
|
|
23
|
+
TeamsInputText,
|
|
24
|
+
TeamsInputToggle,
|
|
25
|
+
TeamsSubmitAction,
|
|
26
|
+
TeamsSubmitData,
|
|
27
|
+
TeamsTable,
|
|
28
|
+
TeamsTableCell,
|
|
29
|
+
TeamsTableColumnDefinition,
|
|
30
|
+
TeamsTableRow,
|
|
31
|
+
TeamsTextBlock,
|
|
32
|
+
TeamsTextSize,
|
|
33
|
+
ToAdaptiveCardOptions,
|
|
34
|
+
} from "./teams/types";
|
package/src/types.ts
CHANGED
|
@@ -1,28 +1,78 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { ZodType } from "zod";
|
|
3
|
+
import type { Action, NormalizedUIElement } from "./ir";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The canonical generative-ui element, normalized from the flat `$type` shape
|
|
7
|
+
* or the legacy `component` shape (see {@link normalizeUINode}). `children` is
|
|
8
|
+
* a reserved top-level key, not a prop, so it lives here rather than inside
|
|
9
|
+
* `props`. Alias of {@link NormalizedUIElement}; kept under its old name so the
|
|
10
|
+
* package's public surface stays append-only.
|
|
11
|
+
*/
|
|
12
|
+
export type GenerativeUIElement = NormalizedUIElement;
|
|
13
|
+
|
|
14
|
+
export type GenerativeUIProps = NormalizedUIElement["props"];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Anything renderable as generative UI — mirrors React's `ReactNode`: an
|
|
18
|
+
* element, primitive text, a list of nodes, or nothing. Widened beyond the
|
|
19
|
+
* model payload (a {@link GenerativeUIElement}) with the renderer's
|
|
20
|
+
* null/boolean/undefined inputs.
|
|
21
|
+
*/
|
|
22
|
+
export type GenerativeUINode =
|
|
23
|
+
| GenerativeUIElement
|
|
24
|
+
| string
|
|
25
|
+
| number
|
|
26
|
+
| boolean
|
|
27
|
+
| null
|
|
28
|
+
| undefined
|
|
29
|
+
| GenerativeUINode[];
|
|
30
|
+
|
|
31
|
+
export type GenerativeUIAction = Action;
|
|
3
32
|
|
|
4
33
|
/** Whether a node's props are still streaming in or have fully arrived. */
|
|
5
34
|
export type GenerativeUIStatus = "streaming" | "done";
|
|
6
35
|
|
|
36
|
+
/** The dispatcher an interactive component calls to fire its `$action`. */
|
|
37
|
+
export type GenerativeUIDispatch = (action: Action) => unknown;
|
|
38
|
+
|
|
7
39
|
/** The render context threaded through {@link renderGenerativeUI}. */
|
|
8
40
|
export type GenerativeUIRenderContext = {
|
|
9
41
|
/** Whether the tool call's arguments are still streaming or are complete. */
|
|
10
42
|
status: GenerativeUIStatus;
|
|
43
|
+
dispatch?: GenerativeUIDispatch;
|
|
11
44
|
};
|
|
12
45
|
|
|
13
46
|
/**
|
|
14
47
|
* Props a component's `render` receives: its model props, rendered `children`,
|
|
15
|
-
* and the injected
|
|
16
|
-
* `"done"`, `P` is complete; while `"streaming"`, `P` is partial.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
48
|
+
* and the injected framework keys. `$status` is the discriminant — when it is
|
|
49
|
+
* `"done"`, `P` is complete; while `"streaming"`, `P` is partial. `$action` is
|
|
50
|
+
* the node's behavior payload, present only on interactive nodes. `$dispatch`
|
|
51
|
+
* fires that payload through the host's {@link ActionRegistry}; it is present
|
|
52
|
+
* only when a registry was wired. All three are `$`-prefixed so they never
|
|
53
|
+
* collide with a real `status`/`action`/`dispatch` prop.
|
|
19
54
|
*/
|
|
20
55
|
type StreamingRenderProps<P> =
|
|
21
|
-
| (Partial<P> & {
|
|
22
|
-
|
|
56
|
+
| (Partial<P> & {
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
$status: "streaming";
|
|
59
|
+
$action?: Action;
|
|
60
|
+
$dispatch?: GenerativeUIDispatch;
|
|
61
|
+
})
|
|
62
|
+
| (P & {
|
|
63
|
+
children?: ReactNode;
|
|
64
|
+
$status: "done";
|
|
65
|
+
$action?: Action;
|
|
66
|
+
$dispatch?: GenerativeUIDispatch;
|
|
67
|
+
});
|
|
23
68
|
|
|
24
69
|
/** Props for a component that only renders once complete — `$status` is always `"done"`. */
|
|
25
|
-
type StaticRenderProps<P> = P & {
|
|
70
|
+
type StaticRenderProps<P> = P & {
|
|
71
|
+
children?: ReactNode;
|
|
72
|
+
$status: "done";
|
|
73
|
+
$action?: Action;
|
|
74
|
+
$dispatch?: GenerativeUIDispatch;
|
|
75
|
+
};
|
|
26
76
|
|
|
27
77
|
/**
|
|
28
78
|
* A component the model is allowed to render, with the schema for its props.
|
|
@@ -67,38 +117,3 @@ export type GenerativeUIComponent<P = any> =
|
|
|
67
117
|
* This registry is the security boundary — any `type` not present is rejected.
|
|
68
118
|
*/
|
|
69
119
|
export type GenerativeUILibrary = Record<string, GenerativeUIComponent>;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A component invocation — mirrors React's `ReactElement`.
|
|
73
|
-
*
|
|
74
|
-
* `type` selects a component from the {@link GenerativeUILibrary}; `props` are
|
|
75
|
-
* passed to it. The `children` prop is special: it is itself a renderable
|
|
76
|
-
* {@link GenerativeUINode}, drawn as generative UI rather than passed as data.
|
|
77
|
-
*
|
|
78
|
-
* On the wire the model emits the flattened form `{ $type, ...props }`, where
|
|
79
|
-
* `$type` names the component so a real `type` prop never collides. This is the
|
|
80
|
-
* normalized shape the renderer works with, the same way React normalizes
|
|
81
|
-
* `createElement` arguments into an element.
|
|
82
|
-
*/
|
|
83
|
-
export type GenerativeUIElement = {
|
|
84
|
-
type: string;
|
|
85
|
-
props: GenerativeUIProps;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** Props passed to a component — mirrors a React component's props. */
|
|
89
|
-
export type GenerativeUIProps = {
|
|
90
|
-
children?: GenerativeUINode;
|
|
91
|
-
} & Record<string, unknown>;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Anything renderable as generative UI — mirrors React's `ReactNode`: an
|
|
95
|
-
* element, primitive text, or a list of nodes.
|
|
96
|
-
*/
|
|
97
|
-
export type GenerativeUINode =
|
|
98
|
-
| GenerativeUIElement
|
|
99
|
-
| string
|
|
100
|
-
| number
|
|
101
|
-
| boolean
|
|
102
|
-
| null
|
|
103
|
-
| undefined
|
|
104
|
-
| GenerativeUINode[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
4
|
+
import { alertVocabulary } from "./alert";
|
|
5
|
+
import { defaultGenerativeUILibrary } from "./index";
|
|
6
|
+
|
|
7
|
+
const render = (node: unknown) =>
|
|
8
|
+
renderToStaticMarkup(<>{renderGenerativeUI(node, alertVocabulary)}</>);
|
|
9
|
+
|
|
10
|
+
describe("alertVocabulary", () => {
|
|
11
|
+
it("Alert renders with title/description/tone, defaulting tone to info", () => {
|
|
12
|
+
expect(
|
|
13
|
+
render({
|
|
14
|
+
$type: "Alert",
|
|
15
|
+
title: "Heads up",
|
|
16
|
+
description: "Something happened",
|
|
17
|
+
}),
|
|
18
|
+
).toBe(
|
|
19
|
+
'<div data-aui="alert" data-aui-tone="info" role="alert"><header data-aui="alert-title">Heads up</header><p data-aui="alert-desc">Something happened</p></div>',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("Alert renders an explicit tone", () => {
|
|
24
|
+
expect(
|
|
25
|
+
render({ $type: "Alert", tone: "danger", title: "Error" }),
|
|
26
|
+
).toContain('data-aui-tone="danger"');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("Carousel wraps each child Card in a slide, capped at 10", () => {
|
|
30
|
+
const children = Array.from({ length: 12 }, (_, i) => ({
|
|
31
|
+
$type: "Card",
|
|
32
|
+
title: `c${i}`,
|
|
33
|
+
}));
|
|
34
|
+
const html = renderToStaticMarkup(
|
|
35
|
+
<>
|
|
36
|
+
{renderGenerativeUI(
|
|
37
|
+
{ $type: "Carousel", children },
|
|
38
|
+
defaultGenerativeUILibrary,
|
|
39
|
+
)}
|
|
40
|
+
</>,
|
|
41
|
+
);
|
|
42
|
+
const slideCount = (html.match(/data-aui="carousel-slide"/g) ?? []).length;
|
|
43
|
+
expect(slideCount).toBe(10);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("Carousel renders a single non-array child Card in one slide", () => {
|
|
47
|
+
const html = renderToStaticMarkup(
|
|
48
|
+
<>
|
|
49
|
+
{renderGenerativeUI(
|
|
50
|
+
{
|
|
51
|
+
$type: "Carousel",
|
|
52
|
+
children: { $type: "Card", title: "only" } as never,
|
|
53
|
+
},
|
|
54
|
+
defaultGenerativeUILibrary,
|
|
55
|
+
)}
|
|
56
|
+
</>,
|
|
57
|
+
);
|
|
58
|
+
expect((html.match(/data-aui="carousel-slide"/g) ?? []).length).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("Carousel without a label renders role=group, no aria-label, and no double-name landmark", () => {
|
|
62
|
+
expect(render({ $type: "Carousel" })).toBe(
|
|
63
|
+
'<div data-aui="carousel" role="group" aria-roledescription="carousel" tabindex="0"></div>',
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("Carousel with a label renders role=region and the given aria-label", () => {
|
|
68
|
+
expect(render({ $type: "Carousel", label: "Featured" })).toBe(
|
|
69
|
+
'<div data-aui="carousel" role="region" aria-roledescription="carousel" aria-label="Featured" tabindex="0"></div>',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("Carousel container carries region semantics and an optional label", () => {
|
|
74
|
+
const html = renderToStaticMarkup(
|
|
75
|
+
<>
|
|
76
|
+
{renderGenerativeUI(
|
|
77
|
+
{
|
|
78
|
+
$type: "Carousel",
|
|
79
|
+
label: "Featured items",
|
|
80
|
+
children: [
|
|
81
|
+
{ $type: "Card", title: "a" },
|
|
82
|
+
{ $type: "Card", title: "b" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
defaultGenerativeUILibrary,
|
|
86
|
+
)}
|
|
87
|
+
</>,
|
|
88
|
+
);
|
|
89
|
+
expect(html).toContain('role="region"');
|
|
90
|
+
expect(html).toContain('aria-roledescription="carousel"');
|
|
91
|
+
expect(html).toContain('aria-label="Featured items"');
|
|
92
|
+
expect(html).toContain('tabindex="0"');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("Carousel slides carry group semantics and a positional label", () => {
|
|
96
|
+
const html = renderToStaticMarkup(
|
|
97
|
+
<>
|
|
98
|
+
{renderGenerativeUI(
|
|
99
|
+
{
|
|
100
|
+
$type: "Carousel",
|
|
101
|
+
label: "Featured items",
|
|
102
|
+
children: [
|
|
103
|
+
{ $type: "Card", title: "a" },
|
|
104
|
+
{ $type: "Card", title: "b" },
|
|
105
|
+
{ $type: "Card", title: "c" },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
defaultGenerativeUILibrary,
|
|
109
|
+
)}
|
|
110
|
+
</>,
|
|
111
|
+
);
|
|
112
|
+
expect((html.match(/role="group"/g) ?? []).length).toBe(3);
|
|
113
|
+
expect((html.match(/aria-roledescription="slide"/g) ?? []).length).toBe(3);
|
|
114
|
+
expect(html).toContain('aria-label="1 of 3"');
|
|
115
|
+
expect(html).toContain('aria-label="2 of 3"');
|
|
116
|
+
expect(html).toContain('aria-label="3 of 3"');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("Carousel slide labels reflect the 10-slide cap, not the raw child count", () => {
|
|
120
|
+
const children = Array.from({ length: 12 }, (_, i) => ({
|
|
121
|
+
$type: "Card",
|
|
122
|
+
title: `c${i}`,
|
|
123
|
+
}));
|
|
124
|
+
const html = renderToStaticMarkup(
|
|
125
|
+
<>
|
|
126
|
+
{renderGenerativeUI(
|
|
127
|
+
{ $type: "Carousel", children },
|
|
128
|
+
defaultGenerativeUILibrary,
|
|
129
|
+
)}
|
|
130
|
+
</>,
|
|
131
|
+
);
|
|
132
|
+
expect(html).toContain('aria-label="1 of 10"');
|
|
133
|
+
expect(html).toContain('aria-label="10 of 10"');
|
|
134
|
+
expect(html).not.toContain("11 of 10");
|
|
135
|
+
});
|
|
136
|
+
});
|