@rudra-js/a2ui 0.8.3 → 0.9.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 +76 -1
- package/dist/recommendation-messages.d.ts +14 -0
- package/dist/recommendation-messages.d.ts.map +1 -0
- package/dist/recommendation-messages.js +242 -0
- package/dist/recommendation-messages.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/package.json +16 -5
- package/src/recommendation-messages.ts +372 -0
- package/src/server.ts +5 -0
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ This package puts that block inside the interfaces an [A2UI](https://a2ui.org) a
|
|
|
10
10
|
adds one component, `RudraRecommendations`, to the catalog your A2UI renderer draws from. The agent
|
|
11
11
|
places it and passes hints about what the shopper is looking at. Your server still picks the
|
|
12
12
|
products and builds the block with `@rudra-js/core`. The browser renders it with `@rudra-js/react`.
|
|
13
|
+
For Flutter, Angular or any other renderer, your server can send the block as A2UI messages
|
|
14
|
+
instead. See [Flutter, Angular and other renderers](#flutter-angular-and-other-renderers).
|
|
13
15
|
|
|
14
16
|
It works with version 0.9 of the A2UI protocol, using `@a2ui/react` and `@a2ui/web_core` 0.14.
|
|
15
17
|
|
|
@@ -28,7 +30,11 @@ already has. Without `@latest`, an app on an older core can get an `ERESOLVE` er
|
|
|
28
30
|
and `react-dom` have no version, so npm and pnpm keep a React 18 app on 18.
|
|
29
31
|
|
|
30
32
|
Everything except zod is a peer dependency, so your app holds one copy of each. npm and pnpm
|
|
31
|
-
install
|
|
33
|
+
install `@rudra-js/core`, `@rudra-js/attested` and `@a2ui/web_core` for you, but not `@a2ui/react`,
|
|
34
|
+
`@rudra-js/react` or `react`. Those three are optional, so a server that only builds
|
|
35
|
+
[messages for other renderers](#flutter-angular-and-other-renderers) can leave React out. The
|
|
36
|
+
browser component can't load without them, so keep them in the command. List the rest anyway,
|
|
37
|
+
because your code imports most of them itself.
|
|
32
38
|
pnpm only lets your code import what your `package.json` names. Yarn doesn't install peers, so on Yarn you
|
|
33
39
|
need the whole list. Yarn also moves a name with no version to its latest release. So on Yarn, if your app
|
|
34
40
|
already has React, leave out `react` and `react-dom`. On pnpm, read [zod](#zod) first.
|
|
@@ -183,6 +189,75 @@ render `RudraComponent` on the server, as the
|
|
|
183
189
|
[`@rudra-js/react` README](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/react)
|
|
184
190
|
describes.
|
|
185
191
|
|
|
192
|
+
## Flutter, Angular and other renderers
|
|
193
|
+
|
|
194
|
+
`RudraRecommendations` needs a React renderer. For any other A2UI renderer, build the block on your
|
|
195
|
+
server as A2UI messages that use only the basic catalog, which every renderer ships. A server with
|
|
196
|
+
no React needs only these:
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
npm install @rudra-js/a2ui@latest @rudra-js/core@latest @rudra-js/attested@latest @a2ui/web_core@0.14
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
If your server has `@rudra-js/anthropic`, add `@rudra-js/anthropic@latest` to that command too.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { buildRecommendationMessages, type RecommendationResult } from '@rudra-js/a2ui/server';
|
|
206
|
+
|
|
207
|
+
export function messagesFor(result: RecommendationResult, locale: string) {
|
|
208
|
+
return buildRecommendationMessages(result, {
|
|
209
|
+
surfaceId: 'rudra_recommendations',
|
|
210
|
+
shopUrl: 'https://shop.example',
|
|
211
|
+
locale,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
In [your endpoint](#your-endpoint), return `Response.json(messagesFor(result, shopper.locale))`
|
|
217
|
+
instead of `result`. Skip the [agent catalog](#the-catalog-your-agent-is-prompted-with) and
|
|
218
|
+
[browser](#the-browser) pieces, and keep `rudraRecommendationsApi` out of your agent's catalog.
|
|
219
|
+
Those are for `RudraRecommendations`, which needs React. Use your own `surfaceId` in place of
|
|
220
|
+
`'rudra_recommendations'`, and never name it in your agent's prompt.
|
|
221
|
+
|
|
222
|
+
Your app can call the endpoint with the four hints and give the messages it gets back to its A2UI
|
|
223
|
+
renderer. Or your agent's code can call it and pass the messages on to the client unchanged. Then
|
|
224
|
+
the call has to carry the shopper's session, because `shopperFor` reads the shopper from the
|
|
225
|
+
request. The first way is the safer one. Either way, keep the model away from the messages and
|
|
226
|
+
from the block's surface, as
|
|
227
|
+
[SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md) explains.
|
|
228
|
+
|
|
229
|
+
- The messages create the surface named `surfaceId` and fill it. To show a new block there, send
|
|
230
|
+
`deleteSurface` for it first. When nothing in the block can be shown, you get no messages.
|
|
231
|
+
- Grids and carousels show two cards to a row. The basic catalog can't fit cards to the screen, and
|
|
232
|
+
its `List` doesn't scroll on every renderer.
|
|
233
|
+
- Each product card is a button that opens the product's page. `hrefForSku` builds the link,
|
|
234
|
+
`/product/<sku>` by default. `shopUrl` resolves that link and any relative image URL, because a
|
|
235
|
+
native app has no page to resolve them against. A product whose image URL is empty or only
|
|
236
|
+
spaces shows no image. A link that is empty, or a link or an image that doesn't end up `http` or
|
|
237
|
+
`https`, throws.
|
|
238
|
+
- A featured card looks like any other, and a banner looks the same whatever its tone, because the
|
|
239
|
+
basic catalog has no styles for them. Nothing carries the `data-rudra-*` attributes the React
|
|
240
|
+
block uses for click tracking, so to count clicks, add a query parameter in `hrefForSku`.
|
|
241
|
+
- Prices are formatted on your server, for `locale`. Without it, they use the server's default
|
|
242
|
+
locale, not the shopper's. Pass `formatPrice` and `formatBundlePrice` to format them yourself.
|
|
243
|
+
- All text uses a heading or caption style, never body, because the web renderers read only body
|
|
244
|
+
text as Markdown. Flutter's `genui` and the A2UI SwiftUI renderer read every style as Markdown, as
|
|
245
|
+
the
|
|
246
|
+
[A2UI guide](https://github.com/a2ui-project/a2ui/blob/main/specification/v0_9_1/docs/basic_catalog_implementation_guide.md)
|
|
247
|
+
asks. So square brackets become parentheses, `<` and `>` become `‹` and `›`, and line breaks
|
|
248
|
+
become spaces. No text can load an image, and only a bare web address or email address can show
|
|
249
|
+
as a link. On SwiftUI a tap opens it. On `genui` it does nothing, and text that starts with `# `
|
|
250
|
+
or `- ` shows as Markdown.
|
|
251
|
+
- The rationale and how the block was generated are never sent.
|
|
252
|
+
- Flutter's `genui` knows the basic catalog by its id from 0.10.3. For 0.8 to 0.10.2, pass
|
|
253
|
+
`catalogId: 'https://a2ui.org/specification/v0_9/basic_catalog.json'`. On Android 11 and later,
|
|
254
|
+
the app has to declare `https` links under `<queries>` in its manifest, or a tap on a card does
|
|
255
|
+
nothing.
|
|
256
|
+
- On `@a2ui/angular` 0.13, register the `BasicCatalog` from `@a2ui/angular/v0_9`, because the one
|
|
257
|
+
from `@a2ui/angular` has the v1.0 id. Pass `useUniversalComponents: true` to `provideA2Ui` too,
|
|
258
|
+
next to `catalogs`. Without it, a click on a card does nothing, and each card tries to open its
|
|
259
|
+
link in a new tab as soon as it renders.
|
|
260
|
+
|
|
186
261
|
## zod
|
|
187
262
|
|
|
188
263
|
A2UI's schemas use zod 3, so this package depends on zod 3. The rest of rudra-js uses zod 4. npm
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { A2uiMessage } from '@a2ui/web_core/v0_9';
|
|
2
|
+
import type { Bundle, Product } from '@rudra-js/core';
|
|
3
|
+
import type { RecommendationResult } from './recommendation-result.js';
|
|
4
|
+
export interface RecommendationMessagesOptions {
|
|
5
|
+
surfaceId: string;
|
|
6
|
+
shopUrl: string;
|
|
7
|
+
catalogId?: string | undefined;
|
|
8
|
+
hrefForSku?: ((sku: string) => string) | undefined;
|
|
9
|
+
formatPrice?: ((product: Product) => string) | undefined;
|
|
10
|
+
formatBundlePrice?: ((bundle: Bundle) => string) | undefined;
|
|
11
|
+
locale?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildRecommendationMessages(result: RecommendationResult, options: RecommendationMessagesOptions): A2uiMessage[];
|
|
14
|
+
//# sourceMappingURL=recommendation-messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendation-messages.d.ts","sourceRoot":"","sources":["../src/recommendation-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAGV,MAAM,EAIN,OAAO,EAER,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAEvE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IACzD,iBAAiB,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7D,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAuTD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,6BAA6B,GACrC,WAAW,EAAE,CAoCf"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const BASIC_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
2
|
+
const CARDS_PER_ROW = 2;
|
|
3
|
+
function defaultHrefForSku(sku) {
|
|
4
|
+
return `/product/${encodeURIComponent(sku)}`;
|
|
5
|
+
}
|
|
6
|
+
function formatMoney(price, currency, locale) {
|
|
7
|
+
let formatter;
|
|
8
|
+
try {
|
|
9
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return `${currency} ${price}`;
|
|
13
|
+
}
|
|
14
|
+
return formatter.format(price);
|
|
15
|
+
}
|
|
16
|
+
function defaultFormatPrice(product, locale) {
|
|
17
|
+
if (!Number.isFinite(product.price)) {
|
|
18
|
+
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number. ` +
|
|
19
|
+
'Catalog objects must satisfy productSchema from @rudra-js/core');
|
|
20
|
+
}
|
|
21
|
+
return formatMoney(product.price, product.currency, locale);
|
|
22
|
+
}
|
|
23
|
+
function defaultFormatBundlePrice(bundle, locale) {
|
|
24
|
+
if (!Number.isFinite(bundle.price)) {
|
|
25
|
+
throw new TypeError(`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number. ` +
|
|
26
|
+
'Bundle objects must satisfy bundleSchema from @rudra-js/core');
|
|
27
|
+
}
|
|
28
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
29
|
+
}
|
|
30
|
+
function toWebUrl(value, base, name) {
|
|
31
|
+
if (typeof value !== 'string')
|
|
32
|
+
throw new TypeError(`${name} is not a string`);
|
|
33
|
+
if (value.trim() === '')
|
|
34
|
+
throw new TypeError(`${name} is empty`);
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(value, base);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new TypeError(`${name} is not a URL: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
43
|
+
throw new TypeError(`${name} is not an http or https URL: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
return url.href;
|
|
46
|
+
}
|
|
47
|
+
function plainText(value) {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/[\t\n\v\f\r \u0085\u2028\u2029]+/g, ' ')
|
|
50
|
+
.trim()
|
|
51
|
+
.replaceAll('[', '(')
|
|
52
|
+
.replaceAll(']', ')')
|
|
53
|
+
.replaceAll('<', '‹')
|
|
54
|
+
.replaceAll('>', '›');
|
|
55
|
+
}
|
|
56
|
+
function defined(ids) {
|
|
57
|
+
return ids.filter((id) => id !== undefined);
|
|
58
|
+
}
|
|
59
|
+
function sellableProduct(context, sku) {
|
|
60
|
+
const product = context.products.get(sku);
|
|
61
|
+
return product?.isInStock === false ? undefined : product;
|
|
62
|
+
}
|
|
63
|
+
function addText(context, id, value, variant) {
|
|
64
|
+
const text = value === null ? '' : plainText(value);
|
|
65
|
+
return text ? context.add({ id, component: 'Text', text, variant }) : undefined;
|
|
66
|
+
}
|
|
67
|
+
function addStartColumn(context, id, children) {
|
|
68
|
+
return context.add({ id, component: 'Column', align: 'start', children: defined(children) });
|
|
69
|
+
}
|
|
70
|
+
function addColumn(context, id, children) {
|
|
71
|
+
return context.add({ id, component: 'Column', children: defined(children) });
|
|
72
|
+
}
|
|
73
|
+
function addLink(context, id, sku, child) {
|
|
74
|
+
const url = context.linkFor(sku);
|
|
75
|
+
return context.add({
|
|
76
|
+
id,
|
|
77
|
+
component: 'Button',
|
|
78
|
+
child,
|
|
79
|
+
variant: 'borderless',
|
|
80
|
+
action: { functionCall: { call: 'openUrl', args: { url }, returnType: 'void' } },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function addLinkedCard(context, id, sku, children, weight) {
|
|
84
|
+
const link = addLink(context, `${id}_link`, sku, addStartColumn(context, `${id}_body`, children));
|
|
85
|
+
return context.add({
|
|
86
|
+
id,
|
|
87
|
+
component: 'Card',
|
|
88
|
+
child: link,
|
|
89
|
+
...(weight === undefined ? {} : { weight }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function addProductCard(context, id, reference, product) {
|
|
93
|
+
const image = product.imageUrl?.trim()
|
|
94
|
+
? context.add({
|
|
95
|
+
id: `${id}_image`,
|
|
96
|
+
component: 'Image',
|
|
97
|
+
url: toWebUrl(product.imageUrl, context.shopUrl, `the image for SKU ${product.sku}`),
|
|
98
|
+
fit: 'cover',
|
|
99
|
+
variant: 'mediumFeature',
|
|
100
|
+
})
|
|
101
|
+
: undefined;
|
|
102
|
+
return addLinkedCard(context, id, product.sku, [
|
|
103
|
+
image,
|
|
104
|
+
addText(context, `${id}_badge`, reference.badge, 'caption'),
|
|
105
|
+
addText(context, `${id}_title`, product.title, 'h5'),
|
|
106
|
+
addText(context, `${id}_price`, context.formatPrice(product), 'h5'),
|
|
107
|
+
addText(context, `${id}_reason`, reference.reason, 'caption'),
|
|
108
|
+
], 1);
|
|
109
|
+
}
|
|
110
|
+
function addCardRows(context, id, title, items) {
|
|
111
|
+
const cards = [];
|
|
112
|
+
for (const reference of items) {
|
|
113
|
+
const product = sellableProduct(context, reference.sku);
|
|
114
|
+
if (product) {
|
|
115
|
+
cards.push(addProductCard(context, `${id}_item${cards.length}`, reference, product));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (cards.length === 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
const rows = [];
|
|
121
|
+
for (let start = 0; start < cards.length; start += CARDS_PER_ROW) {
|
|
122
|
+
const rowId = `${id}_row${rows.length}`;
|
|
123
|
+
const row = cards.slice(start, start + CARDS_PER_ROW);
|
|
124
|
+
while (row.length < CARDS_PER_ROW) {
|
|
125
|
+
row.push(context.add({
|
|
126
|
+
id: `${rowId}_space${row.length}`,
|
|
127
|
+
component: 'Column',
|
|
128
|
+
children: [],
|
|
129
|
+
weight: 1,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
rows.push(context.add({ id: rowId, component: 'Row', align: 'start', children: row }));
|
|
133
|
+
}
|
|
134
|
+
return addColumn(context, id, [addText(context, `${id}_title`, title, 'h3'), ...rows]);
|
|
135
|
+
}
|
|
136
|
+
function addHero(context, id, block) {
|
|
137
|
+
const product = block.sku === null ? undefined : sellableProduct(context, block.sku);
|
|
138
|
+
const headline = addText(context, `${id}_headline`, block.headline, 'h3');
|
|
139
|
+
const body = addText(context, `${id}_body`, block.body, 'caption');
|
|
140
|
+
if (!headline && !body && !product)
|
|
141
|
+
return undefined;
|
|
142
|
+
const card = product
|
|
143
|
+
? addLinkedCard(context, `${id}_product`, product.sku, [
|
|
144
|
+
addText(context, `${id}_product_title`, product.title, 'h4'),
|
|
145
|
+
addText(context, `${id}_product_price`, context.formatPrice(product), 'h5'),
|
|
146
|
+
addText(context, `${id}_product_cta`, block.ctaLabel, 'h5'),
|
|
147
|
+
], undefined)
|
|
148
|
+
: undefined;
|
|
149
|
+
return addStartColumn(context, id, [headline, body, card]);
|
|
150
|
+
}
|
|
151
|
+
function addBanner(context, id, block) {
|
|
152
|
+
const body = addStartColumn(context, `${id}_body`, [
|
|
153
|
+
addText(context, `${id}_text`, block.text, 'h5'),
|
|
154
|
+
addText(context, `${id}_cta`, block.ctaLabel, 'h5'),
|
|
155
|
+
]);
|
|
156
|
+
return context.add({ id, component: 'Card', child: body });
|
|
157
|
+
}
|
|
158
|
+
function addCopy(context, id, block) {
|
|
159
|
+
return addStartColumn(context, id, [
|
|
160
|
+
addText(context, `${id}_title`, block.title, 'h3'),
|
|
161
|
+
addText(context, `${id}_body`, block.body, 'caption'),
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
function addBundle(context, id, block) {
|
|
165
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
166
|
+
if (!bundle)
|
|
167
|
+
return undefined;
|
|
168
|
+
const products = [];
|
|
169
|
+
for (const sku of bundle.skus) {
|
|
170
|
+
const product = sellableProduct(context, sku);
|
|
171
|
+
if (!product)
|
|
172
|
+
return undefined;
|
|
173
|
+
products.push(product);
|
|
174
|
+
}
|
|
175
|
+
const items = addStartColumn(context, `${id}_items`, products.map((product, index) => addLink(context, `${id}_item${index}`, product.sku, context.add({
|
|
176
|
+
id: `${id}_item${index}_title`,
|
|
177
|
+
component: 'Text',
|
|
178
|
+
text: plainText(product.title) || plainText(product.sku),
|
|
179
|
+
variant: 'h5',
|
|
180
|
+
}))));
|
|
181
|
+
const body = addStartColumn(context, `${id}_body`, [
|
|
182
|
+
addText(context, `${id}_label`, bundle.label ?? null, 'h3'),
|
|
183
|
+
addText(context, `${id}_title`, block.title, 'h4'),
|
|
184
|
+
addText(context, `${id}_text`, block.body, 'caption'),
|
|
185
|
+
items,
|
|
186
|
+
addText(context, `${id}_price`, context.formatBundlePrice(bundle), 'h5'),
|
|
187
|
+
addText(context, `${id}_cta`, block.ctaLabel, 'h5'),
|
|
188
|
+
]);
|
|
189
|
+
return context.add({ id, component: 'Card', child: body });
|
|
190
|
+
}
|
|
191
|
+
function addBlock(context, id, block) {
|
|
192
|
+
switch (block.kind) {
|
|
193
|
+
case 'hero':
|
|
194
|
+
return addHero(context, id, block);
|
|
195
|
+
case 'grid':
|
|
196
|
+
case 'carousel':
|
|
197
|
+
return addCardRows(context, id, block.title, block.items);
|
|
198
|
+
case 'banner':
|
|
199
|
+
return addBanner(context, id, block);
|
|
200
|
+
case 'copy':
|
|
201
|
+
return addCopy(context, id, block);
|
|
202
|
+
case 'bundle':
|
|
203
|
+
return addBundle(context, id, block);
|
|
204
|
+
default:
|
|
205
|
+
block;
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function buildRecommendationMessages(result, options) {
|
|
210
|
+
const shopUrl = toWebUrl(options.shopUrl, undefined, 'the shop URL');
|
|
211
|
+
const hrefForSku = options.hrefForSku ?? defaultHrefForSku;
|
|
212
|
+
const components = [];
|
|
213
|
+
const context = {
|
|
214
|
+
products: new Map(result.products.map((product) => [product.sku, product])),
|
|
215
|
+
bundles: new Map((result.bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
216
|
+
shopUrl,
|
|
217
|
+
linkFor: (sku) => toWebUrl(hrefForSku(sku), shopUrl, `the link for SKU ${sku}`),
|
|
218
|
+
formatPrice: options.formatPrice ?? ((product) => defaultFormatPrice(product, options.locale)),
|
|
219
|
+
formatBundlePrice: options.formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, options.locale)),
|
|
220
|
+
add: (component) => {
|
|
221
|
+
components.push(component);
|
|
222
|
+
return component.id;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const blocks = defined(result.spec.blocks.map((block, index) => addBlock(context, `block${index}`, block)));
|
|
226
|
+
if (blocks.length === 0)
|
|
227
|
+
return [];
|
|
228
|
+
addColumn(context, 'root', [
|
|
229
|
+
addText(context, 'headline', result.spec.headline, 'h2'),
|
|
230
|
+
addText(context, 'subheadline', result.spec.subheadline, 'caption'),
|
|
231
|
+
...blocks,
|
|
232
|
+
]);
|
|
233
|
+
const { surfaceId } = options;
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
version: 'v0.9',
|
|
237
|
+
createSurface: { surfaceId, catalogId: options.catalogId ?? BASIC_CATALOG_ID },
|
|
238
|
+
},
|
|
239
|
+
{ version: 'v0.9', updateComponents: { surfaceId, components } },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=recommendation-messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendation-messages.js","sourceRoot":"","sources":["../src/recommendation-messages.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi, } from './recommendations-api.js';
|
|
2
2
|
export { fitRecommendationRequest, parseRecommendationRequest, type RecommendationRequest, } from './recommendation-request.js';
|
|
3
|
+
export { buildRecommendationMessages, type RecommendationMessagesOptions, } from './recommendation-messages.js';
|
|
3
4
|
export type { RecommendationResult } from './recommendation-result.js';
|
|
4
5
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,2BAA2B,EAC3B,KAAK,6BAA6B,GACnC,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi, } from './recommendations-api.js';
|
|
2
2
|
export { fitRecommendationRequest, parseRecommendationRequest, } from './recommendation-request.js';
|
|
3
|
+
export { buildRecommendationMessages, } from './recommendation-messages.js';
|
|
3
4
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,wBAAwB,EACxB,0BAA0B,GAE3B,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,uBAAuB,GACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,wBAAwB,EACxB,0BAA0B,GAE3B,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,2BAA2B,GAE5B,MAAM,8BAA8B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/a2ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A2UI component for rudra-js: lets an A2UI agent place a product-recommendation block whose products, prices and wording come from your shop, not from the agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rudra-js",
|
|
@@ -62,15 +62,26 @@
|
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@a2ui/react": "^0.14.0",
|
|
64
64
|
"@a2ui/web_core": "^0.14.0",
|
|
65
|
-
"@rudra-js/core": "^0.
|
|
66
|
-
"@rudra-js/react": "^0.
|
|
65
|
+
"@rudra-js/core": "^0.9.0",
|
|
66
|
+
"@rudra-js/react": "^0.9.0",
|
|
67
67
|
"react": "^18.2.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@a2ui/react": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@rudra-js/react": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"react": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
69
80
|
"devDependencies": {
|
|
70
81
|
"@a2ui/react": "0.14.0",
|
|
71
82
|
"@a2ui/web_core": "0.14.0",
|
|
72
|
-
"@rudra-js/core": "0.
|
|
73
|
-
"@rudra-js/react": "0.
|
|
83
|
+
"@rudra-js/core": "0.9.0",
|
|
84
|
+
"@rudra-js/react": "0.9.0",
|
|
74
85
|
"@types/react": "^19.3.0",
|
|
75
86
|
"@types/react-dom": "^19.3.0",
|
|
76
87
|
"jsdom": "^27.4.0",
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import type { A2uiMessage } from '@a2ui/web_core/v0_9';
|
|
2
|
+
import type {
|
|
3
|
+
BannerBlock,
|
|
4
|
+
Block,
|
|
5
|
+
Bundle,
|
|
6
|
+
BundleBlock,
|
|
7
|
+
CopyBlock,
|
|
8
|
+
HeroBlock,
|
|
9
|
+
Product,
|
|
10
|
+
ProductReference,
|
|
11
|
+
} from '@rudra-js/core';
|
|
12
|
+
import type { RecommendationResult } from './recommendation-result.js';
|
|
13
|
+
|
|
14
|
+
export interface RecommendationMessagesOptions {
|
|
15
|
+
surfaceId: string;
|
|
16
|
+
shopUrl: string;
|
|
17
|
+
catalogId?: string | undefined;
|
|
18
|
+
hrefForSku?: ((sku: string) => string) | undefined;
|
|
19
|
+
formatPrice?: ((product: Product) => string) | undefined;
|
|
20
|
+
formatBundlePrice?: ((bundle: Bundle) => string) | undefined;
|
|
21
|
+
locale?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BASIC_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
25
|
+
const CARDS_PER_ROW = 2;
|
|
26
|
+
|
|
27
|
+
type TextVariant = 'h2' | 'h3' | 'h4' | 'h5' | 'caption';
|
|
28
|
+
|
|
29
|
+
interface OpenUrlAction {
|
|
30
|
+
functionCall: { call: 'openUrl'; args: { url: string }; returnType: 'void' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type BasicComponent =
|
|
34
|
+
| { id: string; component: 'Text'; text: string; variant: TextVariant }
|
|
35
|
+
| { id: string; component: 'Image'; url: string; fit: 'cover'; variant: 'mediumFeature' }
|
|
36
|
+
| { id: string; component: 'Row'; children: string[]; align: 'start' }
|
|
37
|
+
| { id: string; component: 'Column'; children: string[]; align?: 'start'; weight?: number }
|
|
38
|
+
| { id: string; component: 'Card'; child: string; weight?: number }
|
|
39
|
+
| {
|
|
40
|
+
id: string;
|
|
41
|
+
component: 'Button';
|
|
42
|
+
child: string;
|
|
43
|
+
variant: 'borderless';
|
|
44
|
+
action: OpenUrlAction;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface BuildContext {
|
|
48
|
+
readonly products: ReadonlyMap<string, Product>;
|
|
49
|
+
readonly bundles: ReadonlyMap<string, Bundle>;
|
|
50
|
+
readonly shopUrl: string;
|
|
51
|
+
readonly linkFor: (sku: string) => string;
|
|
52
|
+
readonly formatPrice: (product: Product) => string;
|
|
53
|
+
readonly formatBundlePrice: (bundle: Bundle) => string;
|
|
54
|
+
readonly add: (component: BasicComponent) => string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function defaultHrefForSku(sku: string): string {
|
|
58
|
+
return `/product/${encodeURIComponent(sku)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatMoney(price: number, currency: string, locale: string | undefined): string {
|
|
62
|
+
let formatter: Intl.NumberFormat;
|
|
63
|
+
try {
|
|
64
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
65
|
+
} catch {
|
|
66
|
+
return `${currency} ${price}`;
|
|
67
|
+
}
|
|
68
|
+
return formatter.format(price);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function defaultFormatPrice(product: Product, locale: string | undefined): string {
|
|
72
|
+
if (!Number.isFinite(product.price)) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`price for SKU ${product.sku} is ${String(product.price)}, not a finite number. ` +
|
|
75
|
+
'Catalog objects must satisfy productSchema from @rudra-js/core',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return formatMoney(product.price, product.currency, locale);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function defaultFormatBundlePrice(bundle: Bundle, locale: string | undefined): string {
|
|
82
|
+
if (!Number.isFinite(bundle.price)) {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number. ` +
|
|
85
|
+
'Bundle objects must satisfy bundleSchema from @rudra-js/core',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toWebUrl(value: unknown, base: string | undefined, name: string): string {
|
|
92
|
+
if (typeof value !== 'string') throw new TypeError(`${name} is not a string`);
|
|
93
|
+
if (value.trim() === '') throw new TypeError(`${name} is empty`);
|
|
94
|
+
let url: URL;
|
|
95
|
+
try {
|
|
96
|
+
url = new URL(value, base);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new TypeError(`${name} is not a URL: ${value}`);
|
|
99
|
+
}
|
|
100
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
101
|
+
throw new TypeError(`${name} is not an http or https URL: ${value}`);
|
|
102
|
+
}
|
|
103
|
+
return url.href;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function plainText(value: string): string {
|
|
107
|
+
return value
|
|
108
|
+
.replace(/[\t\n\v\f\r \u0085\u2028\u2029]+/g, ' ')
|
|
109
|
+
.trim()
|
|
110
|
+
.replaceAll('[', '(')
|
|
111
|
+
.replaceAll(']', ')')
|
|
112
|
+
.replaceAll('<', '‹')
|
|
113
|
+
.replaceAll('>', '›');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function defined(ids: readonly (string | undefined)[]): string[] {
|
|
117
|
+
return ids.filter((id): id is string => id !== undefined);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sellableProduct(context: BuildContext, sku: string): Product | undefined {
|
|
121
|
+
const product = context.products.get(sku);
|
|
122
|
+
return product?.isInStock === false ? undefined : product;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addText(
|
|
126
|
+
context: BuildContext,
|
|
127
|
+
id: string,
|
|
128
|
+
value: string | null,
|
|
129
|
+
variant: TextVariant,
|
|
130
|
+
): string | undefined {
|
|
131
|
+
const text = value === null ? '' : plainText(value);
|
|
132
|
+
return text ? context.add({ id, component: 'Text', text, variant }) : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function addStartColumn(
|
|
136
|
+
context: BuildContext,
|
|
137
|
+
id: string,
|
|
138
|
+
children: readonly (string | undefined)[],
|
|
139
|
+
) {
|
|
140
|
+
return context.add({ id, component: 'Column', align: 'start', children: defined(children) });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addColumn(context: BuildContext, id: string, children: readonly (string | undefined)[]) {
|
|
144
|
+
return context.add({ id, component: 'Column', children: defined(children) });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function addLink(context: BuildContext, id: string, sku: string, child: string): string {
|
|
148
|
+
const url = context.linkFor(sku);
|
|
149
|
+
return context.add({
|
|
150
|
+
id,
|
|
151
|
+
component: 'Button',
|
|
152
|
+
child,
|
|
153
|
+
variant: 'borderless',
|
|
154
|
+
action: { functionCall: { call: 'openUrl', args: { url }, returnType: 'void' } },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addLinkedCard(
|
|
159
|
+
context: BuildContext,
|
|
160
|
+
id: string,
|
|
161
|
+
sku: string,
|
|
162
|
+
children: readonly (string | undefined)[],
|
|
163
|
+
weight: number | undefined,
|
|
164
|
+
): string {
|
|
165
|
+
const link = addLink(context, `${id}_link`, sku, addStartColumn(context, `${id}_body`, children));
|
|
166
|
+
return context.add({
|
|
167
|
+
id,
|
|
168
|
+
component: 'Card',
|
|
169
|
+
child: link,
|
|
170
|
+
...(weight === undefined ? {} : { weight }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addProductCard(
|
|
175
|
+
context: BuildContext,
|
|
176
|
+
id: string,
|
|
177
|
+
reference: ProductReference,
|
|
178
|
+
product: Product,
|
|
179
|
+
): string {
|
|
180
|
+
const image = product.imageUrl?.trim()
|
|
181
|
+
? context.add({
|
|
182
|
+
id: `${id}_image`,
|
|
183
|
+
component: 'Image',
|
|
184
|
+
url: toWebUrl(product.imageUrl, context.shopUrl, `the image for SKU ${product.sku}`),
|
|
185
|
+
fit: 'cover',
|
|
186
|
+
variant: 'mediumFeature',
|
|
187
|
+
})
|
|
188
|
+
: undefined;
|
|
189
|
+
return addLinkedCard(
|
|
190
|
+
context,
|
|
191
|
+
id,
|
|
192
|
+
product.sku,
|
|
193
|
+
[
|
|
194
|
+
image,
|
|
195
|
+
addText(context, `${id}_badge`, reference.badge, 'caption'),
|
|
196
|
+
addText(context, `${id}_title`, product.title, 'h5'),
|
|
197
|
+
addText(context, `${id}_price`, context.formatPrice(product), 'h5'),
|
|
198
|
+
addText(context, `${id}_reason`, reference.reason, 'caption'),
|
|
199
|
+
],
|
|
200
|
+
1,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function addCardRows(
|
|
205
|
+
context: BuildContext,
|
|
206
|
+
id: string,
|
|
207
|
+
title: string | null,
|
|
208
|
+
items: readonly ProductReference[],
|
|
209
|
+
): string | undefined {
|
|
210
|
+
const cards: string[] = [];
|
|
211
|
+
for (const reference of items) {
|
|
212
|
+
const product = sellableProduct(context, reference.sku);
|
|
213
|
+
if (product) {
|
|
214
|
+
cards.push(addProductCard(context, `${id}_item${cards.length}`, reference, product));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (cards.length === 0) return undefined;
|
|
218
|
+
|
|
219
|
+
const rows: string[] = [];
|
|
220
|
+
for (let start = 0; start < cards.length; start += CARDS_PER_ROW) {
|
|
221
|
+
const rowId = `${id}_row${rows.length}`;
|
|
222
|
+
const row = cards.slice(start, start + CARDS_PER_ROW);
|
|
223
|
+
while (row.length < CARDS_PER_ROW) {
|
|
224
|
+
row.push(
|
|
225
|
+
context.add({
|
|
226
|
+
id: `${rowId}_space${row.length}`,
|
|
227
|
+
component: 'Column',
|
|
228
|
+
children: [],
|
|
229
|
+
weight: 1,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
rows.push(context.add({ id: rowId, component: 'Row', align: 'start', children: row }));
|
|
234
|
+
}
|
|
235
|
+
return addColumn(context, id, [addText(context, `${id}_title`, title, 'h3'), ...rows]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function addHero(context: BuildContext, id: string, block: HeroBlock): string | undefined {
|
|
239
|
+
const product = block.sku === null ? undefined : sellableProduct(context, block.sku);
|
|
240
|
+
const headline = addText(context, `${id}_headline`, block.headline, 'h3');
|
|
241
|
+
const body = addText(context, `${id}_body`, block.body, 'caption');
|
|
242
|
+
if (!headline && !body && !product) return undefined;
|
|
243
|
+
|
|
244
|
+
const card = product
|
|
245
|
+
? addLinkedCard(
|
|
246
|
+
context,
|
|
247
|
+
`${id}_product`,
|
|
248
|
+
product.sku,
|
|
249
|
+
[
|
|
250
|
+
addText(context, `${id}_product_title`, product.title, 'h4'),
|
|
251
|
+
addText(context, `${id}_product_price`, context.formatPrice(product), 'h5'),
|
|
252
|
+
addText(context, `${id}_product_cta`, block.ctaLabel, 'h5'),
|
|
253
|
+
],
|
|
254
|
+
undefined,
|
|
255
|
+
)
|
|
256
|
+
: undefined;
|
|
257
|
+
return addStartColumn(context, id, [headline, body, card]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function addBanner(context: BuildContext, id: string, block: BannerBlock): string {
|
|
261
|
+
const body = addStartColumn(context, `${id}_body`, [
|
|
262
|
+
addText(context, `${id}_text`, block.text, 'h5'),
|
|
263
|
+
addText(context, `${id}_cta`, block.ctaLabel, 'h5'),
|
|
264
|
+
]);
|
|
265
|
+
return context.add({ id, component: 'Card', child: body });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function addCopy(context: BuildContext, id: string, block: CopyBlock): string {
|
|
269
|
+
return addStartColumn(context, id, [
|
|
270
|
+
addText(context, `${id}_title`, block.title, 'h3'),
|
|
271
|
+
addText(context, `${id}_body`, block.body, 'caption'),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function addBundle(context: BuildContext, id: string, block: BundleBlock): string | undefined {
|
|
276
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
277
|
+
if (!bundle) return undefined;
|
|
278
|
+
|
|
279
|
+
const products: Product[] = [];
|
|
280
|
+
for (const sku of bundle.skus) {
|
|
281
|
+
const product = sellableProduct(context, sku);
|
|
282
|
+
if (!product) return undefined;
|
|
283
|
+
products.push(product);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const items = addStartColumn(
|
|
287
|
+
context,
|
|
288
|
+
`${id}_items`,
|
|
289
|
+
products.map((product, index) =>
|
|
290
|
+
addLink(
|
|
291
|
+
context,
|
|
292
|
+
`${id}_item${index}`,
|
|
293
|
+
product.sku,
|
|
294
|
+
context.add({
|
|
295
|
+
id: `${id}_item${index}_title`,
|
|
296
|
+
component: 'Text',
|
|
297
|
+
text: plainText(product.title) || plainText(product.sku),
|
|
298
|
+
variant: 'h5',
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
const body = addStartColumn(context, `${id}_body`, [
|
|
304
|
+
addText(context, `${id}_label`, bundle.label ?? null, 'h3'),
|
|
305
|
+
addText(context, `${id}_title`, block.title, 'h4'),
|
|
306
|
+
addText(context, `${id}_text`, block.body, 'caption'),
|
|
307
|
+
items,
|
|
308
|
+
addText(context, `${id}_price`, context.formatBundlePrice(bundle), 'h5'),
|
|
309
|
+
addText(context, `${id}_cta`, block.ctaLabel, 'h5'),
|
|
310
|
+
]);
|
|
311
|
+
return context.add({ id, component: 'Card', child: body });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function addBlock(context: BuildContext, id: string, block: Block): string | undefined {
|
|
315
|
+
switch (block.kind) {
|
|
316
|
+
case 'hero':
|
|
317
|
+
return addHero(context, id, block);
|
|
318
|
+
case 'grid':
|
|
319
|
+
case 'carousel':
|
|
320
|
+
return addCardRows(context, id, block.title, block.items);
|
|
321
|
+
case 'banner':
|
|
322
|
+
return addBanner(context, id, block);
|
|
323
|
+
case 'copy':
|
|
324
|
+
return addCopy(context, id, block);
|
|
325
|
+
case 'bundle':
|
|
326
|
+
return addBundle(context, id, block);
|
|
327
|
+
default:
|
|
328
|
+
block satisfies never;
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function buildRecommendationMessages(
|
|
334
|
+
result: RecommendationResult,
|
|
335
|
+
options: RecommendationMessagesOptions,
|
|
336
|
+
): A2uiMessage[] {
|
|
337
|
+
const shopUrl = toWebUrl(options.shopUrl, undefined, 'the shop URL');
|
|
338
|
+
const hrefForSku = options.hrefForSku ?? defaultHrefForSku;
|
|
339
|
+
const components: BasicComponent[] = [];
|
|
340
|
+
const context: BuildContext = {
|
|
341
|
+
products: new Map(result.products.map((product) => [product.sku, product])),
|
|
342
|
+
bundles: new Map((result.bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
343
|
+
shopUrl,
|
|
344
|
+
linkFor: (sku) => toWebUrl(hrefForSku(sku), shopUrl, `the link for SKU ${sku}`),
|
|
345
|
+
formatPrice: options.formatPrice ?? ((product) => defaultFormatPrice(product, options.locale)),
|
|
346
|
+
formatBundlePrice:
|
|
347
|
+
options.formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, options.locale)),
|
|
348
|
+
add: (component) => {
|
|
349
|
+
components.push(component);
|
|
350
|
+
return component.id;
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const blocks = defined(
|
|
355
|
+
result.spec.blocks.map((block, index) => addBlock(context, `block${index}`, block)),
|
|
356
|
+
);
|
|
357
|
+
if (blocks.length === 0) return [];
|
|
358
|
+
|
|
359
|
+
addColumn(context, 'root', [
|
|
360
|
+
addText(context, 'headline', result.spec.headline, 'h2'),
|
|
361
|
+
addText(context, 'subheadline', result.spec.subheadline, 'caption'),
|
|
362
|
+
...blocks,
|
|
363
|
+
]);
|
|
364
|
+
const { surfaceId } = options;
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
version: 'v0.9',
|
|
368
|
+
createSurface: { surfaceId, catalogId: options.catalogId ?? BASIC_CATALOG_ID },
|
|
369
|
+
},
|
|
370
|
+
{ version: 'v0.9', updateComponents: { surfaceId, components } },
|
|
371
|
+
];
|
|
372
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -9,4 +9,9 @@ export {
|
|
|
9
9
|
type RecommendationRequest,
|
|
10
10
|
} from './recommendation-request.js';
|
|
11
11
|
|
|
12
|
+
export {
|
|
13
|
+
buildRecommendationMessages,
|
|
14
|
+
type RecommendationMessagesOptions,
|
|
15
|
+
} from './recommendation-messages.js';
|
|
16
|
+
|
|
12
17
|
export type { RecommendationResult } from './recommendation-result.js';
|