@rudra-js/a2ui 0.8.3 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,12 +6,15 @@ rudra-js is an open-source TypeScript library (MIT) you install from npm. A lang
6
6
 
7
7
  It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
8
8
 
9
- This package puts that block inside the interfaces an [A2UI](https://a2ui.org) agent builds. It
10
- adds one component, `RudraRecommendations`, to the catalog your A2UI renderer draws from. The agent
11
- places it and passes hints about what the shopper is looking at. Your server still picks the
12
- products and builds the block with `@rudra-js/core`. The browser renders it with `@rudra-js/react`.
9
+ You can use this package to put the block inside the interfaces an [A2UI](https://a2ui.org) agent
10
+ builds. It adds a single component, `RudraRecommendations`, to the catalog your A2UI renderer draws
11
+ from. This component will be placed by the agent, which will also pass hints about what the shopper
12
+ is looking at. It's still up to your server to pick the products and build the block with
13
+ `@rudra-js/core`. To render the block in the browser, this package uses `@rudra-js/react`. For
14
+ Flutter, Angular or any other renderer, your server can send the block as A2UI messages instead (see
15
+ [Flutter, Angular and other renderers](#flutter-angular-and-other-renderers)).
13
16
 
14
- It works with version 0.9 of the A2UI protocol, using `@a2ui/react` and `@a2ui/web_core` 0.14.
17
+ It works with version 0.9 of the A2UI protocol and uses `@a2ui/react` and `@a2ui/web_core` 0.14.
15
18
 
16
19
  ## Install
17
20
 
@@ -19,29 +22,37 @@ It works with version 0.9 of the A2UI protocol, using `@a2ui/react` and `@a2ui/w
19
22
  npm install @rudra-js/a2ui@latest @rudra-js/core@latest @rudra-js/react@latest @rudra-js/attested@latest @a2ui/react@0.14 @a2ui/web_core@0.14 react react-dom
20
23
  ```
21
24
 
22
- Use the same command to install and to upgrade, whether or not your app already uses rudra-js or A2UI.
23
- If your app has `@rudra-js/anthropic`, add `@rudra-js/anthropic@latest` to that command.
24
-
25
- The `@rudra-js` packages are released together. Each one's peer range for the others starts at its
26
- own version. For a package name with no version, npm stays inside the range your `package.json`
27
- already has. Without `@latest`, an app on an older core can get an `ERESOLVE` error. `react`
28
- and `react-dom` have no version, so npm and pnpm keep a React 18 app on 18.
29
-
30
- Everything except zod is a peer dependency, so your app holds one copy of each. npm and pnpm
31
- install peers for you. List them anyway, because your code imports most of them itself.
32
- pnpm only lets your code import what your `package.json` names. Yarn doesn't install peers, so on Yarn you
33
- need the whole list. Yarn also moves a name with no version to its latest release. So on Yarn, if your app
34
- already has React, leave out `react` and `react-dom`. On pnpm, read [zod](#zod) first.
35
-
36
- Keep `skipLibCheck: true` in your `tsconfig.json`. Without it, A2UI's own type declarations fail
37
- to typecheck. They need `"dom"` in `lib`. Two of their imports also have no file extension, which
38
- `nodenext` refuses.
25
+ To install and also to upgrade, you will need to use this same command. It doesn't matter if your
26
+ app already uses rudra-js or A2UI.
27
+ Any app that has `@rudra-js/anthropic` will also need to add `@rudra-js/anthropic@latest` to this
28
+ command.
29
+
30
+ All the `@rudra-js` packages are released together. For each package, the peer range for the others
31
+ starts at the version of that package. For a package name with no version, npm will stay inside the
32
+ range your `package.json` already has. If `@latest` is left out, an app on an older core can get an
33
+ `ERESOLVE` error. With no version on `react` and `react-dom`, npm and pnpm will keep a React 18 app
34
+ on 18.
35
+
36
+ Because everything except zod is a peer dependency, your app holds a single copy of each. On npm and
37
+ pnpm, `@rudra-js/core`, `@rudra-js/attested` and `@a2ui/web_core` will be installed for you, but not
38
+ `@a2ui/react`, `@rudra-js/react` or `react`. React can be left out on a server that only builds
39
+ [messages for other renderers](#flutter-angular-and-other-renderers), since those three are
40
+ optional. But those three have to stay in the command for the browser component to load. List the
41
+ rest anyway, because your code imports most of these itself.
42
+ With pnpm your code can only import the packages your `package.json` names. You will need the whole
43
+ list on Yarn because it doesn't install peers. It will also move a name with no version to its
44
+ latest release. So if your app already has React, leave out `react` and `react-dom` on Yarn. On
45
+ pnpm, read [zod](#zod) first.
46
+
47
+ A2UI's type declarations will fail to typecheck if you don't keep `skipLibCheck: true` in your
48
+ `tsconfig.json`. They need `"dom"` in `lib`. Two of their imports will also be refused by `nodenext`
49
+ because these have no file extension.
39
50
 
40
51
  ## Three pieces
41
52
 
42
53
  ### The catalog your agent is prompted with
43
54
 
44
- Put `agentCatalog.toJson()` in your agent's prompt. Have the agent create surfaces with
55
+ Put `agentCatalog.toJson()` in your agent's prompt and have the agent create surfaces with
45
56
  `catalogId: agentCatalog.id`. If you have catalog instructions of your own, join them to
46
57
  `RUDRA_RECOMMENDATIONS_INSTRUCTIONS`.
47
58
 
@@ -59,15 +70,16 @@ export const agentCatalog = new Catalog(
59
70
  );
60
71
  ```
61
72
 
62
- The first `toJson()` call logs one "Recursive reference detected" warning per hint. It's harmless.
73
+ You will see a "Recursive reference detected" warning per hint from the first `toJson()` call. It's
74
+ harmless.
63
75
 
64
76
  ### Your endpoint
65
77
 
66
- `shopperFor` and `candidatesFor` stand in for the session and catalog code you already have.
67
- `provider: null` calls no model, so pass your own provider.
78
+ `shopperFor` and `candidatesFor` stand in for the session and catalog code you already have. You
79
+ will need to pass your own provider, because `provider: null` calls no model.
68
80
 
69
- `generation` defaults to `'cohort'`, which shares one block design between shoppers who look alike. In
70
- that mode the model never sees `searchQuery`. The agent's search only matters with
81
+ By default `generation` is `'cohort'`. This shares a single block design between shoppers who look
82
+ alike. In that mode the model never sees `searchQuery`. For the agent's search to matter, you need
71
83
  `generation: 'per-shopper'`.
72
84
 
73
85
  ```ts
@@ -107,13 +119,13 @@ export async function POST(request: Request): Promise<Response> {
107
119
  ```
108
120
 
109
121
  - `parseRecommendationRequest` accepts only `currentSku`, `currentCategory`, `searchQuery` and
110
- `maxItems`. Anything else gets a 400.
111
- - `fitRecommendationRequest` drops a SKU or category that none of your candidates has.
112
- - Pick candidates on your side, not from the hints.
113
- - Each new category, `maxItems` or candidate set means a new model call. With `per-shopper`, so
114
- does each shopper and each new SKU or search. Rate-limit the endpoint (see
115
- [SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md)).
116
- - Send back `input.candidates` and `input.bundles`, not your raw catalog.
122
+ `maxItems`. Anything else will get a 400.
123
+ - `fitRecommendationRequest` will drop a SKU or category if it's not in any of your candidates.
124
+ - Pick candidates on your side instead of from the hints.
125
+ - There will be a new model call for each new category, `maxItems` or candidate set. With
126
+ `per-shopper` this also goes for each shopper and each new SKU or search. Rate-limit the endpoint
127
+ (see [SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md)).
128
+ - Send back `input.candidates` and `input.bundles` instead of your raw catalog.
117
129
 
118
130
  ### The browser
119
131
 
@@ -147,56 +159,129 @@ const shopCatalog = new Catalog<ReactCatalogComponent>(
147
159
  export const processor = new MessageProcessor<ReactCatalogComponent>([shopCatalog]);
148
160
  ```
149
161
 
150
- Give the browser catalog the same id as the agent catalog. A2UI looks it up by the `catalogId` the
151
- agent sends. Render the processor's surfaces with `A2uiSurface`.
162
+ Give the browser catalog the id of the agent catalog, because A2UI will look it up by the
163
+ `catalogId` the agent sends. Use `A2uiSurface` to render the surfaces of the processor.
152
164
 
153
165
  `createRudraRecommendationsComponent` takes `loadRecommendations`, `placeholder` and `onError`. It
154
- also takes every `RudraComponent` prop except `spec`, `products` and `bundles`.
166
+ also takes all of the `RudraComponent` props except `spec`, `products` and `bundles`.
155
167
 
156
168
  ## What the component does
157
169
 
158
- - It reads each hint either as a literal value or from the agent's data model, like
159
- `{ "path": "/viewing/sku" }`. It sends only the four hints the endpoint accepts.
160
- - It leaves out a hint that is blank, or one the server would turn away, like a `maxItems` of 40.
161
- - A literal of the wrong type, like a numeric SKU, makes A2UI refuse the agent's whole message.
162
- - It waits until the hints stop changing for 300 ms, then calls `loadRecommendations`.
170
+ - It will read each hint either as a literal value or from the agent's data model, like
171
+ `{ "path": "/viewing/sku" }`. It will only send the four hints the endpoint accepts.
172
+ - It leaves out any hint that is blank. It also leaves out a hint the server would turn away, like a
173
+ `maxItems` of 40.
174
+ - With a literal of the wrong type, like a numeric SKU, A2UI will refuse the agent's whole message.
175
+ - It waits for the hints to stop changing for 300 ms and then calls `loadRecommendations`.
163
176
  - It keeps showing the last block until the next one arrives.
164
177
  - `signal` aborts when the hints change or the component unmounts.
165
- - It checks the result against core's schemas before it renders any of it.
178
+ - It will check the result against core's schemas before it renders any of it.
166
179
  - It shows `placeholder` until the first load finishes.
167
- - If a load fails or the result fails the schema check, it shows nothing and calls `onError`. It
168
- does the same if the block throws while rendering.
180
+ - If a load fails or the result fails the schema check, it will show nothing and call `onError`.
181
+ This is also what happens if the block throws while rendering.
169
182
  - Each component on a surface sends its own request.
170
183
 
171
184
  ## Where it runs
172
185
 
173
- `@rudra-js/a2ui` holds `createRudraRecommendationsComponent` and its types. It loads only in the
174
- browser, because `@a2ui/react` needs a DOM. The agent catalog and your endpoint import from
175
- `@rudra-js/a2ui/server`, which runs anywhere.
186
+ `@rudra-js/a2ui` holds `createRudraRecommendationsComponent` and its types. It can only be loaded in
187
+ the browser, because `@a2ui/react` needs a DOM. For the agent catalog and your endpoint, import from
188
+ `@rudra-js/a2ui/server`. This entry point can run anywhere.
176
189
 
177
- In Next.js, a `'use client'` file still runs on the server for the first render. Load the file
178
- that imports `@rudra-js/a2ui` through `next/dynamic` with `ssr: false`, and make that call from a
179
- client component.
190
+ In Next.js, a `'use client'` file will also run on the server for the first render. To load the file
191
+ that imports `@rudra-js/a2ui`, you will need `next/dynamic` with `ssr: false`. This call has to be
192
+ made from a client component.
180
193
 
181
- A2UI renders in the browser, so a crawler won't see a block an agent places. For a product page,
182
- render `RudraComponent` on the server, as the
194
+ Any block an agent places won't be seen by a crawler, because A2UI renders in the browser. For a
195
+ product page, render `RudraComponent` on the server as the
183
196
  [`@rudra-js/react` README](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/react)
184
197
  describes.
185
198
 
199
+ ## Flutter, Angular and other renderers
200
+
201
+ `RudraRecommendations` needs a React renderer. For any other A2UI renderer, build the block on your
202
+ server as A2UI messages that use just the basic catalog. All renderers ship with this catalog. If
203
+ your server has no React, it only needs these:
204
+
205
+ ```sh
206
+ npm install @rudra-js/a2ui@latest @rudra-js/core@latest @rudra-js/attested@latest @a2ui/web_core@0.14
207
+ ```
208
+
209
+ If your server has `@rudra-js/anthropic`, add `@rudra-js/anthropic@latest` to that command too.
210
+
211
+ ```ts
212
+ import { buildRecommendationMessages, type RecommendationResult } from '@rudra-js/a2ui/server';
213
+
214
+ export function messagesFor(result: RecommendationResult, locale: string) {
215
+ return buildRecommendationMessages(result, {
216
+ surfaceId: 'rudra_recommendations',
217
+ shopUrl: 'https://shop.example',
218
+ locale,
219
+ });
220
+ }
221
+ ```
222
+
223
+ In [your endpoint](#your-endpoint), return `Response.json(messagesFor(result, shopper.locale))`
224
+ instead of `result`. Skip the [agent catalog](#the-catalog-your-agent-is-prompted-with) and
225
+ [browser](#the-browser) pieces and keep `rudraRecommendationsApi` out of your agent's catalog,
226
+ because they are all for `RudraRecommendations`, which needs React. Use your own `surfaceId` in
227
+ place of `'rudra_recommendations'`. It must never be named in your agent's prompt.
228
+
229
+ Your app can call the endpoint with the four hints and give the messages it gets back to its A2UI
230
+ renderer. This is the safer way. Your agent's code can also call it and pass the messages on to the
231
+ client unchanged, but then the call has to carry the shopper's session because `shopperFor` reads
232
+ the shopper from the request. Either way, keep the model away from the messages and from the surface
233
+ of the block, as [SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md)
234
+ explains.
235
+
236
+ - These messages will create the surface named `surfaceId` and then fill it. To show a new block
237
+ there, first send `deleteSurface` for this surface. You will get no messages if nothing in the
238
+ block can be shown.
239
+ - Grids and carousels show two cards to a row, because the basic catalog can't fit cards to the
240
+ screen and its `List` doesn't scroll on all renderers.
241
+ - Each product card is a button to open the product's page. By default `hrefForSku` builds the link
242
+ as `/product/<sku>`. `shopUrl` will resolve this link and any relative image URL, because a native
243
+ app has no page to resolve them against. If the image URL of a product is empty or only spaces, no
244
+ image will be shown. An empty link will throw. A link or an image that doesn't end up `http` or
245
+ `https` will also throw.
246
+ - Featured cards look like any other card and banners look the same in all tones, because the basic
247
+ catalog has no styles for them. As nothing carries the `data-rudra-*` attributes the React block
248
+ uses for click tracking, you will need to add a query parameter in `hrefForSku` to count clicks.
249
+ - Prices are formatted on your server for `locale`. If you leave it out, they will use the default
250
+ locale of the server and not the shopper's. You can also pass `formatPrice` and
251
+ `formatBundlePrice` to format prices yourself.
252
+ - All text uses a heading or caption style and never body, because the web renderers only read body
253
+ text as Markdown. Flutter's `genui` and the A2UI SwiftUI renderer read all styles as Markdown, as
254
+ the
255
+ [A2UI guide](https://github.com/a2ui-project/a2ui/blob/main/specification/v0_9_1/docs/basic_catalog_implementation_guide.md)
256
+ asks. So square brackets become parentheses, while `<` and `>` become `‹` and `›`. Line breaks
257
+ also become spaces. No text can load an image. Only a bare web address or email address can show
258
+ as a link. On SwiftUI a tap will open the link, but on `genui` it does nothing and text that
259
+ starts with `#` or `-` followed by a space shows as Markdown.
260
+ - Neither the rationale nor how the block was generated is ever sent.
261
+ - Flutter's `genui` knows the basic catalog by its id from 0.10.3. For 0.8 to 0.10.2, pass
262
+ `catalogId: 'https://a2ui.org/specification/v0_9/basic_catalog.json'`. On Android 11 and later, a
263
+ tap on a card will do nothing if the app doesn't declare `https` links under `<queries>` in its
264
+ manifest.
265
+ - On `@a2ui/angular` 0.13, register the `BasicCatalog` from `@a2ui/angular/v0_9` because the catalog
266
+ from `@a2ui/angular` has the v1.0 id. You also have to pass `useUniversalComponents: true` to
267
+ `provideA2Ui` next to `catalogs`. If you don't, a click on a card will do nothing and all cards
268
+ will try to open their links in a new tab as soon as they render.
269
+
186
270
  ## zod
187
271
 
188
- A2UI's schemas use zod 3, so this package depends on zod 3. The rest of rudra-js uses zod 4. npm
189
- installs both, each where its own package finds it. With npm, you don't need either one yourself.
272
+ This package depends on zod 3 because A2UI's schemas use zod 3, but the rest of rudra-js uses zod 4.
273
+ npm will install both, each where its own package can find it. With npm you don't need to add either
274
+ version yourself.
190
275
 
191
- On pnpm, `@a2ui/react` gets core's zod 4 as its zod peer. Then the browser example fails to
192
- typecheck with TS2344. If your app has no zod of its own, add zod 3:
276
+ On pnpm, `@a2ui/react` gets core's zod 4 as its zod peer. This makes the browser example fail to
277
+ typecheck with TS2344. If your app has no zod of its own, you will need to add zod 3:
193
278
 
194
279
  ```sh
195
280
  pnpm add zod@^3.25.76
196
281
  ```
197
282
 
198
- If your app uses zod 4, remove the zod peer from `@a2ui/react` in `.pnpmfile.cjs`. pnpm then gives
199
- it the zod 3 it depends on:
283
+ If your app uses zod 4, you will need to remove the zod peer from `@a2ui/react` in `.pnpmfile.cjs`.
284
+ Then pnpm will give it the zod 3 it depends on:
200
285
 
201
286
  ```js
202
287
  module.exports = {
@@ -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
@@ -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
@@ -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.8.3",
3
+ "version": "0.9.1",
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.8.3",
66
- "@rudra-js/react": "^0.8.3",
65
+ "@rudra-js/core": "^0.9.1",
66
+ "@rudra-js/react": "^0.9.1",
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.8.3",
73
- "@rudra-js/react": "0.8.3",
83
+ "@rudra-js/core": "0.9.1",
84
+ "@rudra-js/react": "0.9.1",
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';