@rudra-js/a2ui 0.0.0 → 0.8.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clive Dsouza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @rudra-js/a2ui
2
+
3
+ This package is part of [rudra-js](https://rudrajs.com).
4
+
5
+ rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.
6
+
7
+ It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
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 hints at what the shopper is looking at. Your server still picks the products and
12
+ builds the block with `@rudra-js/core`, and the browser renders it with `@rudra-js/react`.
13
+
14
+ It works with version 0.9 of the A2UI protocol, through `@a2ui/react` and `@a2ui/web_core` 0.14.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ 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
+ ```
21
+
22
+ Run the same command in a new app, in one that already uses rudra-js or A2UI, and to upgrade. If
23
+ your app has `@rudra-js/anthropic`, add `@rudra-js/anthropic@latest` to the command. The
24
+ `@rudra-js` packages are published together, and each one's peer ranges on the others start at its
25
+ own version. For a bare name npm stays inside the range your `package.json` already has, so
26
+ without `@latest` an app on an older core can get an `ERESOLVE`. `@0.14` is the `@a2ui` release
27
+ this package works with. `react` and `react-dom` carry no version, so npm and pnpm keep a React 18
28
+ app on 18.
29
+
30
+ Everything but zod is a peer dependency, so your app holds one copy of each. npm and pnpm add
31
+ peers for you, but your code imports most of them itself, and pnpm lets it import only what your
32
+ `package.json` names. Yarn doesn't add peers, so there you need the whole list. It does move a bare
33
+ name to its latest version, so leave out `react` and `react-dom` if your app already has React. On
34
+ pnpm, read [zod](#zod) first.
35
+
36
+ Keep `skipLibCheck: true`. Without it, A2UI's own declarations fail to typecheck: they need `"dom"`
37
+ in `lib`, and two of their imports have no file extension, which `nodenext` refuses.
38
+
39
+ ## Three pieces
40
+
41
+ ### The catalog your agent is prompted with
42
+
43
+ Put `agentCatalog.toJson()` in your agent's prompt and have it create surfaces with
44
+ `catalogId: agentCatalog.id`. If you have catalog instructions of your own, join them to
45
+ `RUDRA_RECOMMENDATIONS_INSTRUCTIONS`.
46
+
47
+ ```ts
48
+ import { Catalog } from '@a2ui/web_core/v0_9';
49
+ import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
50
+ import { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi } from '@rudra-js/a2ui/server';
51
+
52
+ export const agentCatalog = new Catalog(
53
+ 'https://shop.example/a2ui/catalog.json',
54
+ 'v0.9',
55
+ [...basicCatalog.components.values(), rudraRecommendationsApi],
56
+ [...basicCatalog.functions.values()],
57
+ { instructions: RUDRA_RECOMMENDATIONS_INSTRUCTIONS },
58
+ );
59
+ ```
60
+
61
+ The first `toJson()` logs one "Recursive reference detected" warning per hint. It's harmless.
62
+
63
+ ### Your endpoint
64
+
65
+ `shopperFor` and `candidatesFor` stand for the session and catalog code you already have.
66
+ `provider: null` calls no model, so pass your own provider. In the default cohort mode the model
67
+ never sees `searchQuery`, so the agent's search only counts with `generation: 'per-shopper'`.
68
+
69
+ ```ts
70
+ import { createComponentGenerator, parseTrackingInput } from '@rudra-js/core';
71
+ import {
72
+ fitRecommendationRequest,
73
+ parseRecommendationRequest,
74
+ type RecommendationRequest,
75
+ type RecommendationResult,
76
+ } from '@rudra-js/a2ui/server';
77
+ import { candidatesFor, shopperFor } from './your-shop.js';
78
+
79
+ const generator = createComponentGenerator({ provider: null });
80
+
81
+ export async function POST(request: Request): Promise<Response> {
82
+ let asked: RecommendationRequest;
83
+ try {
84
+ asked = parseRecommendationRequest(await request.json());
85
+ } catch {
86
+ return new Response('Bad request', { status: 400 });
87
+ }
88
+
89
+ const shopper = await shopperFor(request);
90
+ const candidates = await candidatesFor(shopper);
91
+ const hints = fitRecommendationRequest(asked, candidates);
92
+ const input = parseTrackingInput({
93
+ user: { id: shopper.id },
94
+ context: { surface: 'assistant', locale: shopper.locale, ...hints },
95
+ candidates,
96
+ signals: shopper.signals,
97
+ });
98
+ const spec = await generator.generate(input);
99
+
100
+ const result: RecommendationResult = { spec, products: input.candidates, bundles: input.bundles };
101
+ return Response.json(result);
102
+ }
103
+ ```
104
+
105
+ - `parseRecommendationRequest` accepts only `currentSku`, `currentCategory`, `searchQuery` and
106
+ `maxItems`. Anything else is a 400.
107
+ - `fitRecommendationRequest` drops a SKU or category none of your candidates has.
108
+ - Pick candidates on your side, not from the hints. Each new category, `maxItems` or candidate set
109
+ is a new model call, and with `per-shopper` so is each shopper and each new SKU or search, so
110
+ rate-limit the endpoint (see [SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md)).
111
+ - Send back `input.candidates` and `input.bundles`, not your raw catalog.
112
+
113
+ ### The browser
114
+
115
+ ```tsx
116
+ import type { ReactCatalogComponent } from '@a2ui/react/v0_9';
117
+ import { Catalog, MessageProcessor } from '@a2ui/web_core/v0_9';
118
+ import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
119
+ import { createRudraRecommendationsComponent } from '@rudra-js/a2ui';
120
+
121
+ const recommendations = createRudraRecommendationsComponent({
122
+ async loadRecommendations(request, signal) {
123
+ const response = await fetch('/api/recommendations', {
124
+ method: 'POST',
125
+ headers: { 'content-type': 'application/json' },
126
+ body: JSON.stringify(request),
127
+ signal,
128
+ });
129
+ if (!response.ok) throw new Error(`recommendations: HTTP ${response.status}`);
130
+ return response.json();
131
+ },
132
+ locale: 'en-US',
133
+ });
134
+
135
+ const shopCatalog = new Catalog<ReactCatalogComponent>(
136
+ 'https://shop.example/a2ui/catalog.json',
137
+ 'v0.9',
138
+ [...basicCatalog.components.values(), recommendations],
139
+ [...basicCatalog.functions.values()],
140
+ );
141
+
142
+ export const processor = new MessageProcessor<ReactCatalogComponent>([shopCatalog]);
143
+ ```
144
+
145
+ Give the browser catalog the same id as the agent catalog, because A2UI looks it up by the
146
+ `catalogId` the agent sends. Render the processor's surfaces with `A2uiSurface`. The options take
147
+ `loadRecommendations`, `placeholder`, `onError`, and every `RudraComponent` prop except `spec`,
148
+ `products` and `bundles`.
149
+
150
+ ## What the component does
151
+
152
+ - It reads each hint as a literal or from the agent's data model (`{ "path": "/viewing/sku" }`),
153
+ and sends only the four. It leaves out a blank one, and one the server would turn away, like a
154
+ `maxItems` of 40. A literal of the wrong type, like a numeric SKU, makes A2UI refuse the agent's
155
+ whole message.
156
+ - It asks once the hints hold still for 300 ms, and keeps the last block up until the next one
157
+ arrives. `signal` aborts when the hints change or the component unmounts.
158
+ - It checks the result against core's schemas before rendering any of it.
159
+ - It shows `placeholder` until the first load. On a failed load, a bad result or a render error it
160
+ shows nothing and calls `onError`.
161
+ - Each component on a surface sends its own request.
162
+
163
+ ## Where it runs
164
+
165
+ `@rudra-js/a2ui` holds `createRudraRecommendationsComponent` and its types, and loads only in the
166
+ browser, because `@a2ui/react` needs a DOM. The agent catalog and your endpoint import from
167
+ `@rudra-js/a2ui/server`, which runs anywhere. In Next.js a `'use client'` file still runs on the
168
+ server for the first render, so load the one that imports `@rudra-js/a2ui` from a client component
169
+ with `next/dynamic` and `ssr: false`.
170
+
171
+ A2UI renders on the client, so a crawler won't see a block an agent places. For a product page,
172
+ render `RudraComponent` on the server, as the
173
+ [`@rudra-js/react` README](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/react)
174
+ describes.
175
+
176
+ ## zod
177
+
178
+ A2UI's schemas are zod 3, so this package depends on zod 3 while the rest of rudra-js uses zod 4.
179
+ npm installs both, each where its package finds it, and you don't need either one yourself.
180
+
181
+ On pnpm, `@a2ui/react` gets core's zod 4 as its zod peer, and the browser example stops
182
+ typechecking (TS2344). If your app has no zod of its own, add zod 3:
183
+
184
+ ```sh
185
+ pnpm add zod@^3.25.76
186
+ ```
187
+
188
+ If your app uses zod 4, drop the peer from `@a2ui/react` in `.pnpmfile.cjs`, and pnpm gives it
189
+ the zod 3 it depends on:
190
+
191
+ ```js
192
+ module.exports = {
193
+ hooks: {
194
+ readPackage(manifest) {
195
+ if (manifest.name === '@a2ui/react') delete manifest.peerDependencies?.zod;
196
+ return manifest;
197
+ },
198
+ },
199
+ };
200
+ ```
201
+
202
+ ## Licence
203
+
204
+ [MIT](./LICENSE)
@@ -0,0 +1,4 @@
1
+ export { createRudraRecommendationsComponent, type RecommendationLoader, type RudraRecommendationsOptions, } from './recommendations-component.js';
2
+ export type { RecommendationRequest } from './recommendation-request.js';
3
+ export type { RecommendationResult } from './recommendation-result.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,EACnC,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAC;AAExC,YAAY,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACzE,YAAY,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createRudraRecommendationsComponent, } from './recommendations-component.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,GAGpC,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Product } from '@rudra-js/core';
2
+ export interface RecommendationRequest {
3
+ currentSku?: string | undefined;
4
+ currentCategory?: string | undefined;
5
+ searchQuery?: string | undefined;
6
+ maxItems?: number | undefined;
7
+ }
8
+ export declare function parseRecommendationRequest(value: unknown): RecommendationRequest;
9
+ export declare function fitRecommendationRequest(request: RecommendationRequest, candidates: readonly Pick<Product, 'sku' | 'category'>[]): RecommendationRequest;
10
+ export declare function pickRecommendationRequest(written: Readonly<Record<string, unknown>>, resolved: Readonly<Record<string, unknown>>): RecommendationRequest;
11
+ //# sourceMappingURL=recommendation-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendation-request.d.ts","sourceRoot":"","sources":["../src/recommendation-request.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAMD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAEhF;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,SAAS,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,UAAU,CAAC,EAAE,GACvD,qBAAqB,CAUvB;AAKD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1C,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC1C,qBAAqB,CAQvB"}
@@ -0,0 +1,34 @@
1
+ import { renderContextSchema } from '@rudra-js/core/schemas';
2
+ const recommendationRequestSchema = renderContextSchema
3
+ .pick({ currentSku: true, currentCategory: true, searchQuery: true })
4
+ .extend({ maxItems: renderContextSchema.shape.maxItems.unwrap().optional() });
5
+ export function parseRecommendationRequest(value) {
6
+ return recommendationRequestSchema.parse(value);
7
+ }
8
+ export function fitRecommendationRequest(request, candidates) {
9
+ const { currentSku, currentCategory, searchQuery, maxItems } = request;
10
+ const fitted = {};
11
+ if (candidates.some((candidate) => candidate.sku === currentSku))
12
+ fitted.currentSku = currentSku;
13
+ if (candidates.some((candidate) => candidate.category === currentCategory)) {
14
+ fitted.currentCategory = currentCategory;
15
+ }
16
+ if (searchQuery !== undefined)
17
+ fitted.searchQuery = searchQuery;
18
+ if (maxItems !== undefined)
19
+ fitted.maxItems = maxItems;
20
+ return fitted;
21
+ }
22
+ const isBlank = (value) => value === undefined || (typeof value === 'string' && value.trim() === '');
23
+ export function pickRecommendationRequest(written, resolved) {
24
+ const request = {};
25
+ for (const [field, schema] of Object.entries(recommendationRequestSchema.shape)) {
26
+ if (!Object.hasOwn(written, field))
27
+ continue;
28
+ const checked = schema.safeParse(resolved[field]);
29
+ if (checked.success && !isBlank(checked.data))
30
+ request[field] = checked.data;
31
+ }
32
+ return recommendationRequestSchema.parse(request);
33
+ }
34
+ //# sourceMappingURL=recommendation-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendation-request.js","sourceRoot":"","sources":["../src/recommendation-request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAU7D,MAAM,2BAA2B,GAAG,mBAAmB;KACpD,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;KACpE,MAAM,CAAC,EAAE,QAAQ,EAAE,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAEhF,MAAM,UAAU,0BAA0B,CAAC,KAAc;IACvD,OAAO,2BAA2B,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,OAA8B,EAC9B,UAAwD;IAExD,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IACvE,MAAM,MAAM,GAA0B,EAAE,CAAC;IACzC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,UAAU,CAAC;QAAE,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC;IACjG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,eAAe,CAAC,EAAE,CAAC;QAC3E,MAAM,CAAC,eAAe,GAAG,eAAe,CAAC;IAC3C,CAAC;IACD,IAAI,WAAW,KAAK,SAAS;QAAE,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;IAChE,IAAI,QAAQ,KAAK,SAAS;QAAE,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACvD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,KAAc,EAAW,EAAE,CAC1C,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAE5E,MAAM,UAAU,yBAAyB,CACvC,OAA0C,EAC1C,QAA2C;IAE3C,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,2BAA2B,CAAC,KAAK,CAAC,EAAE,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC;YAAE,SAAS;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAClD,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAC/E,CAAC;IACD,OAAO,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Bundle, ComponentSpec, Product } from '@rudra-js/core';
2
+ export interface RecommendationResult {
3
+ spec: ComponentSpec;
4
+ products: readonly Product[];
5
+ bundles?: readonly Bundle[];
6
+ }
7
+ export declare function parseRecommendationResult(value: unknown): RecommendationResult;
8
+ //# sourceMappingURL=recommendation-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendation-result.d.ts","sourceRoot":"","sources":["../src/recommendation-result.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAErE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAU9E"}
@@ -0,0 +1,13 @@
1
+ import { bundleSchema, parseComponentSpec, productSchema } from '@rudra-js/core/schemas';
2
+ export function parseRecommendationResult(value) {
3
+ if (typeof value !== 'object' || value === null) {
4
+ throw new TypeError('the recommendation result is not an object');
5
+ }
6
+ const { spec, products, bundles } = value;
7
+ return {
8
+ spec: parseComponentSpec(spec),
9
+ products: productSchema.array().parse(products),
10
+ bundles: bundleSchema.array().default([]).parse(bundles),
11
+ };
12
+ }
13
+ //# sourceMappingURL=recommendation-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendation-result.js","sourceRoot":"","sources":["../src/recommendation-result.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AASzF,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAgC,CAAC;IACrE,OAAO;QACL,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC;QAC9B,QAAQ,EAAE,aAAa,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC/C,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;KACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod/v3';
2
+ export declare const RUDRA_RECOMMENDATIONS_INSTRUCTIONS: string;
3
+ export declare const rudraRecommendationsApi: {
4
+ name: string;
5
+ schema: z.ZodObject<{
6
+ currentSku: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
7
+ path: z.ZodString;
8
+ }, "strict", z.ZodTypeAny, {
9
+ path: string;
10
+ }, {
11
+ path: string;
12
+ }>, z.ZodType<import("@a2ui/web_core/v0_9").FunctionCall, z.ZodTypeDef, import("@a2ui/web_core/v0_9").FunctionCall>]>>;
13
+ currentCategory: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
14
+ path: z.ZodString;
15
+ }, "strict", z.ZodTypeAny, {
16
+ path: string;
17
+ }, {
18
+ path: string;
19
+ }>, z.ZodType<import("@a2ui/web_core/v0_9").FunctionCall, z.ZodTypeDef, import("@a2ui/web_core/v0_9").FunctionCall>]>>;
20
+ searchQuery: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
21
+ path: z.ZodString;
22
+ }, "strict", z.ZodTypeAny, {
23
+ path: string;
24
+ }, {
25
+ path: string;
26
+ }>, z.ZodType<import("@a2ui/web_core/v0_9").FunctionCall, z.ZodTypeDef, import("@a2ui/web_core/v0_9").FunctionCall>]>>;
27
+ maxItems: z.ZodOptional<z.ZodUnion<[z.ZodNumber, z.ZodObject<{
28
+ path: z.ZodString;
29
+ }, "strict", z.ZodTypeAny, {
30
+ path: string;
31
+ }, {
32
+ path: string;
33
+ }>, z.ZodType<import("@a2ui/web_core/v0_9").FunctionCall, z.ZodTypeDef, import("@a2ui/web_core/v0_9").FunctionCall>]>>;
34
+ }, "strip", z.ZodTypeAny, {
35
+ currentSku?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
36
+ path: string;
37
+ } | undefined;
38
+ currentCategory?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
39
+ path: string;
40
+ } | undefined;
41
+ searchQuery?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
42
+ path: string;
43
+ } | undefined;
44
+ maxItems?: number | import("@a2ui/web_core/v0_9").FunctionCall | {
45
+ path: string;
46
+ } | undefined;
47
+ }, {
48
+ currentSku?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
49
+ path: string;
50
+ } | undefined;
51
+ currentCategory?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
52
+ path: string;
53
+ } | undefined;
54
+ searchQuery?: string | import("@a2ui/web_core/v0_9").FunctionCall | {
55
+ path: string;
56
+ } | undefined;
57
+ maxItems?: number | import("@a2ui/web_core/v0_9").FunctionCall | {
58
+ path: string;
59
+ } | undefined;
60
+ }>;
61
+ };
62
+ //# sourceMappingURL=recommendations-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendations-api.d.ts","sourceRoot":"","sources":["../src/recommendations-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAI3B,eAAO,MAAM,kCAAkC,QAIP,CAAC;AAmBzC,eAAO,MAAM,uBAAuB;IAClC,IAAI;IACJ,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CACP,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod/v3';
2
+ import { CommonSchemas } from '@a2ui/web_core/v0_9';
3
+ import { FIELD_LIMITS } from '@rudra-js/core/schemas';
4
+ export const RUDRA_RECOMMENDATIONS_INSTRUCTIONS = 'RudraRecommendations shows product recommendations from this shop. Place it and ' +
5
+ 'say what the shopper is looking at. The shop chooses the products and writes the heading, and ' +
6
+ "every title, price, image and link comes from the shop's catalog. Do not write product names, " +
7
+ 'prices, ratings or offers around it.';
8
+ const recommendationsPropertiesSchema = z.object({
9
+ currentSku: CommonSchemas.DynamicString.optional().describe('SKU of the product the shopper is looking at or asking about. It is left out of the ' +
10
+ 'recommendations. Use only a SKU the shop gave you.'),
11
+ currentCategory: CommonSchemas.DynamicString.optional().describe('Catalog category the shopper is browsing or asking about.'),
12
+ searchQuery: CommonSchemas.DynamicString.optional().describe("What the shopper searched for or asked for, in the shopper's own words."),
13
+ maxItems: CommonSchemas.DynamicNumber.optional().describe(`Most products to show, a whole number from 1 to ${FIELD_LIMITS.maxItems}. The shop decides ` +
14
+ 'when this is left out or out of range.'),
15
+ });
16
+ export const rudraRecommendationsApi = {
17
+ name: 'RudraRecommendations',
18
+ schema: recommendationsPropertiesSchema,
19
+ };
20
+ //# sourceMappingURL=recommendations-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendations-api.js","sourceRoot":"","sources":["../src/recommendations-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,MAAM,CAAC,MAAM,kCAAkC,GAC7C,kFAAkF;IAClF,gGAAgG;IAChG,gGAAgG;IAChG,sCAAsC,CAAC;AAEzC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,UAAU,EAAE,aAAa,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,QAAQ,CACzD,sFAAsF;QACpF,oDAAoD,CACvD;IACD,eAAe,EAAE,aAAa,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAC9D,2DAA2D,CAC5D;IACD,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAC1D,yEAAyE,CAC1E;IACD,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,QAAQ,CACvD,mDAAmD,YAAY,CAAC,QAAQ,qBAAqB;QAC3F,wCAAwC,CAC3C;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,IAAI,EAAE,sBAAsB;IAC5B,MAAM,EAAE,+BAA+B;CACxC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ReactComponentImplementation } from '@a2ui/react/v0_9';
3
+ import { type RudraComponentProps } from '@rudra-js/react';
4
+ import { type RecommendationRequest } from './recommendation-request.js';
5
+ import { type RecommendationResult } from './recommendation-result.js';
6
+ export type RecommendationLoader = (request: RecommendationRequest, signal: AbortSignal) => Promise<RecommendationResult>;
7
+ export interface RudraRecommendationsOptions extends Omit<RudraComponentProps, 'spec' | 'products' | 'bundles'> {
8
+ loadRecommendations: RecommendationLoader;
9
+ placeholder?: ReactNode;
10
+ onError?: (error: unknown) => void;
11
+ }
12
+ export declare function createRudraRecommendationsComponent({ loadRecommendations, placeholder, onError, ...blockProps }: RudraRecommendationsOptions): ReactComponentImplementation;
13
+ //# sourceMappingURL=recommendations-component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendations-component.d.ts","sourceRoot":"","sources":["../src/recommendations-component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAiC,KAAK,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAEpG,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpG,OAAO,EAA6B,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,CACjC,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEnC,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,mBAAmB,EACnB,MAAM,GAAG,UAAU,GAAG,SAAS,CAChC;IACC,mBAAmB,EAAE,oBAAoB,CAAC;IAC1C,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA+ED,wBAAgB,mCAAmC,CAAC,EAClD,mBAAmB,EACnB,WAAkB,EAClB,OAAO,EACP,GAAG,UAAU,EACd,EAAE,2BAA2B,GAAG,4BAA4B,CAiB5D"}
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Component, useCallback, useEffect, useRef, useState, useSyncExternalStore, } from 'react';
3
+ import { createComponentImplementation } from '@a2ui/react/v0_9';
4
+ import { RudraComponent } from '@rudra-js/react';
5
+ import { rudraRecommendationsApi } from './recommendations-api.js';
6
+ import { pickRecommendationRequest } from './recommendation-request.js';
7
+ import { parseRecommendationResult } from './recommendation-result.js';
8
+ const HINT_SETTLE_MS = 300;
9
+ function useWrittenProperties(model) {
10
+ const subscribe = useCallback((onChange) => {
11
+ const subscription = model.onUpdated.subscribe(onChange);
12
+ return () => subscription.unsubscribe();
13
+ }, [model]);
14
+ return useSyncExternalStore(subscribe, () => model.properties);
15
+ }
16
+ function useRecommendationResult(request, loadRecommendations, onError) {
17
+ const [state, setState] = useState({ status: 'loading' });
18
+ const shownRequestKey = useRef(null);
19
+ const requestKey = JSON.stringify(request);
20
+ useEffect(() => {
21
+ if (requestKey === shownRequestKey.current)
22
+ return;
23
+ const controller = new AbortController();
24
+ const asked = JSON.parse(requestKey);
25
+ async function load() {
26
+ try {
27
+ const result = parseRecommendationResult(await loadRecommendations(asked, controller.signal));
28
+ if (controller.signal.aborted)
29
+ return;
30
+ shownRequestKey.current = requestKey;
31
+ setState({ status: 'ready', result, requestKey });
32
+ }
33
+ catch (error) {
34
+ if (controller.signal.aborted)
35
+ return;
36
+ shownRequestKey.current = null;
37
+ setState({ status: 'failed' });
38
+ onError?.(error);
39
+ }
40
+ }
41
+ const timer = setTimeout(load, HINT_SETTLE_MS);
42
+ return () => {
43
+ clearTimeout(timer);
44
+ controller.abort();
45
+ };
46
+ }, [requestKey, loadRecommendations, onError]);
47
+ return state;
48
+ }
49
+ class RenderFailureBoundary extends Component {
50
+ state = { hasFailed: false };
51
+ static getDerivedStateFromError() {
52
+ return { hasFailed: true };
53
+ }
54
+ componentDidCatch(error) {
55
+ this.props.onError?.(error);
56
+ }
57
+ render() {
58
+ return this.state.hasFailed ? null : this.props.children;
59
+ }
60
+ }
61
+ export function createRudraRecommendationsComponent({ loadRecommendations, placeholder = null, onError, ...blockProps }) {
62
+ return createComponentImplementation(rudraRecommendationsApi, function RudraRecommendations({ props, context }) {
63
+ const written = useWrittenProperties(context.componentModel);
64
+ const request = pickRecommendationRequest(written, props);
65
+ const state = useRecommendationResult(request, loadRecommendations, onError);
66
+ if (state.status === 'loading')
67
+ return placeholder;
68
+ if (state.status === 'failed')
69
+ return null;
70
+ return (_jsx(RenderFailureBoundary, { onError: onError, children: _jsx(RudraComponent, { ...blockProps, ...state.result }) }, state.requestKey));
71
+ });
72
+ }
73
+ //# sourceMappingURL=recommendations-component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendations-component.js","sourceRoot":"","sources":["../src/recommendations-component.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi, } from './recommendations-api.js';
2
+ export { fitRecommendationRequest, parseRecommendationRequest, type RecommendationRequest, } from './recommendation-request.js';
3
+ export type { RecommendationResult } from './recommendation-result.js';
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +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"}
package/dist/server.js ADDED
@@ -0,0 +1,3 @@
1
+ export { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi, } from './recommendations-api.js';
2
+ export { fitRecommendationRequest, parseRecommendationRequest, } from './recommendation-request.js';
3
+ //# sourceMappingURL=server.js.map
@@ -0,0 +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"}
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@rudra-js/a2ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder. The real package ships from github.com/clivedsouza1010/rudra-js.",
5
- "license": "MIT"
3
+ "version": "0.8.2",
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
+ "keywords": [
6
+ "rudra-js",
7
+ "a2ui",
8
+ "product-recommendations",
9
+ "react",
10
+ "llm",
11
+ "agents",
12
+ "generative-ui",
13
+ "ecommerce",
14
+ "typescript"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Clive Dsouza",
18
+ "homepage": "https://rudrajs.com",
19
+ "bugs": {
20
+ "url": "https://github.com/clivedsouza1010/rudra-js/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/clivedsouza1010/rudra-js.git",
25
+ "directory": "packages/a2ui"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.12.0"
29
+ },
30
+ "type": "module",
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./server": {
39
+ "types": "./dist/server.d.ts",
40
+ "default": "./dist/server.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src/**/*.ts",
47
+ "src/**/*.tsx",
48
+ "!src/**/*.test.ts",
49
+ "!src/**/*.test.tsx"
50
+ ],
51
+ "sideEffects": false,
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "prepack": "npm run build"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "dependencies": {
60
+ "zod": "^3.25.76"
61
+ },
62
+ "peerDependencies": {
63
+ "@a2ui/react": "^0.14.0",
64
+ "@a2ui/web_core": "^0.14.0",
65
+ "@rudra-js/core": "^0.8.2",
66
+ "@rudra-js/react": "^0.8.2",
67
+ "react": "^18.2.0 || ^19.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@a2ui/react": "0.14.0",
71
+ "@a2ui/web_core": "0.14.0",
72
+ "@rudra-js/core": "0.8.2",
73
+ "@rudra-js/react": "0.8.2",
74
+ "@types/react": "^19.3.0",
75
+ "@types/react-dom": "^19.3.0",
76
+ "jsdom": "^27.4.0",
77
+ "react": "^19.3.0",
78
+ "react-dom": "^19.3.0"
79
+ }
6
80
  }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ createRudraRecommendationsComponent,
3
+ type RecommendationLoader,
4
+ type RudraRecommendationsOptions,
5
+ } from './recommendations-component.js';
6
+
7
+ export type { RecommendationRequest } from './recommendation-request.js';
8
+ export type { RecommendationResult } from './recommendation-result.js';
@@ -0,0 +1,48 @@
1
+ import { renderContextSchema } from '@rudra-js/core/schemas';
2
+ import type { Product } from '@rudra-js/core';
3
+
4
+ export interface RecommendationRequest {
5
+ currentSku?: string | undefined;
6
+ currentCategory?: string | undefined;
7
+ searchQuery?: string | undefined;
8
+ maxItems?: number | undefined;
9
+ }
10
+
11
+ const recommendationRequestSchema = renderContextSchema
12
+ .pick({ currentSku: true, currentCategory: true, searchQuery: true })
13
+ .extend({ maxItems: renderContextSchema.shape.maxItems.unwrap().optional() });
14
+
15
+ export function parseRecommendationRequest(value: unknown): RecommendationRequest {
16
+ return recommendationRequestSchema.parse(value);
17
+ }
18
+
19
+ export function fitRecommendationRequest(
20
+ request: RecommendationRequest,
21
+ candidates: readonly Pick<Product, 'sku' | 'category'>[],
22
+ ): RecommendationRequest {
23
+ const { currentSku, currentCategory, searchQuery, maxItems } = request;
24
+ const fitted: RecommendationRequest = {};
25
+ if (candidates.some((candidate) => candidate.sku === currentSku)) fitted.currentSku = currentSku;
26
+ if (candidates.some((candidate) => candidate.category === currentCategory)) {
27
+ fitted.currentCategory = currentCategory;
28
+ }
29
+ if (searchQuery !== undefined) fitted.searchQuery = searchQuery;
30
+ if (maxItems !== undefined) fitted.maxItems = maxItems;
31
+ return fitted;
32
+ }
33
+
34
+ const isBlank = (value: unknown): boolean =>
35
+ value === undefined || (typeof value === 'string' && value.trim() === '');
36
+
37
+ export function pickRecommendationRequest(
38
+ written: Readonly<Record<string, unknown>>,
39
+ resolved: Readonly<Record<string, unknown>>,
40
+ ): RecommendationRequest {
41
+ const request: Record<string, unknown> = {};
42
+ for (const [field, schema] of Object.entries(recommendationRequestSchema.shape)) {
43
+ if (!Object.hasOwn(written, field)) continue;
44
+ const checked = schema.safeParse(resolved[field]);
45
+ if (checked.success && !isBlank(checked.data)) request[field] = checked.data;
46
+ }
47
+ return recommendationRequestSchema.parse(request);
48
+ }
@@ -0,0 +1,20 @@
1
+ import { bundleSchema, parseComponentSpec, productSchema } from '@rudra-js/core/schemas';
2
+ import type { Bundle, ComponentSpec, Product } from '@rudra-js/core';
3
+
4
+ export interface RecommendationResult {
5
+ spec: ComponentSpec;
6
+ products: readonly Product[];
7
+ bundles?: readonly Bundle[];
8
+ }
9
+
10
+ export function parseRecommendationResult(value: unknown): RecommendationResult {
11
+ if (typeof value !== 'object' || value === null) {
12
+ throw new TypeError('the recommendation result is not an object');
13
+ }
14
+ const { spec, products, bundles } = value as Record<string, unknown>;
15
+ return {
16
+ spec: parseComponentSpec(spec),
17
+ products: productSchema.array().parse(products),
18
+ bundles: bundleSchema.array().default([]).parse(bundles),
19
+ };
20
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod/v3';
2
+ import { CommonSchemas } from '@a2ui/web_core/v0_9';
3
+ import { FIELD_LIMITS } from '@rudra-js/core/schemas';
4
+
5
+ export const RUDRA_RECOMMENDATIONS_INSTRUCTIONS =
6
+ 'RudraRecommendations shows product recommendations from this shop. Place it and ' +
7
+ 'say what the shopper is looking at. The shop chooses the products and writes the heading, and ' +
8
+ "every title, price, image and link comes from the shop's catalog. Do not write product names, " +
9
+ 'prices, ratings or offers around it.';
10
+
11
+ const recommendationsPropertiesSchema = z.object({
12
+ currentSku: CommonSchemas.DynamicString.optional().describe(
13
+ 'SKU of the product the shopper is looking at or asking about. It is left out of the ' +
14
+ 'recommendations. Use only a SKU the shop gave you.',
15
+ ),
16
+ currentCategory: CommonSchemas.DynamicString.optional().describe(
17
+ 'Catalog category the shopper is browsing or asking about.',
18
+ ),
19
+ searchQuery: CommonSchemas.DynamicString.optional().describe(
20
+ "What the shopper searched for or asked for, in the shopper's own words.",
21
+ ),
22
+ maxItems: CommonSchemas.DynamicNumber.optional().describe(
23
+ `Most products to show, a whole number from 1 to ${FIELD_LIMITS.maxItems}. The shop decides ` +
24
+ 'when this is left out or out of range.',
25
+ ),
26
+ });
27
+
28
+ export const rudraRecommendationsApi = {
29
+ name: 'RudraRecommendations',
30
+ schema: recommendationsPropertiesSchema,
31
+ };
@@ -0,0 +1,130 @@
1
+ import {
2
+ Component,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ useSyncExternalStore,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import { createComponentImplementation, type ReactComponentImplementation } from '@a2ui/react/v0_9';
11
+ import type { ComponentModel } from '@a2ui/web_core/v0_9';
12
+ import { RudraComponent, type RudraComponentProps } from '@rudra-js/react';
13
+ import { rudraRecommendationsApi } from './recommendations-api.js';
14
+ import { pickRecommendationRequest, type RecommendationRequest } from './recommendation-request.js';
15
+ import { parseRecommendationResult, type RecommendationResult } from './recommendation-result.js';
16
+
17
+ export type RecommendationLoader = (
18
+ request: RecommendationRequest,
19
+ signal: AbortSignal,
20
+ ) => Promise<RecommendationResult>;
21
+
22
+ export interface RudraRecommendationsOptions extends Omit<
23
+ RudraComponentProps,
24
+ 'spec' | 'products' | 'bundles'
25
+ > {
26
+ loadRecommendations: RecommendationLoader;
27
+ placeholder?: ReactNode;
28
+ onError?: (error: unknown) => void;
29
+ }
30
+
31
+ const HINT_SETTLE_MS = 300;
32
+
33
+ type LoadState =
34
+ | { status: 'loading' }
35
+ | { status: 'failed' }
36
+ | { status: 'ready'; result: RecommendationResult; requestKey: string };
37
+
38
+ function useWrittenProperties(model: ComponentModel): Readonly<Record<string, unknown>> {
39
+ const subscribe = useCallback(
40
+ (onChange: () => void) => {
41
+ const subscription = model.onUpdated.subscribe(onChange);
42
+ return () => subscription.unsubscribe();
43
+ },
44
+ [model],
45
+ );
46
+ return useSyncExternalStore(subscribe, () => model.properties);
47
+ }
48
+
49
+ function useRecommendationResult(
50
+ request: RecommendationRequest,
51
+ loadRecommendations: RecommendationLoader,
52
+ onError: RudraRecommendationsOptions['onError'],
53
+ ): LoadState {
54
+ const [state, setState] = useState<LoadState>({ status: 'loading' });
55
+ const shownRequestKey = useRef<string | null>(null);
56
+ const requestKey = JSON.stringify(request);
57
+
58
+ useEffect(() => {
59
+ if (requestKey === shownRequestKey.current) return;
60
+ const controller = new AbortController();
61
+ const asked: RecommendationRequest = JSON.parse(requestKey);
62
+
63
+ async function load() {
64
+ try {
65
+ const result = parseRecommendationResult(
66
+ await loadRecommendations(asked, controller.signal),
67
+ );
68
+ if (controller.signal.aborted) return;
69
+ shownRequestKey.current = requestKey;
70
+ setState({ status: 'ready', result, requestKey });
71
+ } catch (error) {
72
+ if (controller.signal.aborted) return;
73
+ shownRequestKey.current = null;
74
+ setState({ status: 'failed' });
75
+ onError?.(error);
76
+ }
77
+ }
78
+
79
+ const timer = setTimeout(load, HINT_SETTLE_MS);
80
+ return () => {
81
+ clearTimeout(timer);
82
+ controller.abort();
83
+ };
84
+ }, [requestKey, loadRecommendations, onError]);
85
+
86
+ return state;
87
+ }
88
+
89
+ class RenderFailureBoundary extends Component<
90
+ { onError: RudraRecommendationsOptions['onError']; children: ReactNode },
91
+ { hasFailed: boolean }
92
+ > {
93
+ override state = { hasFailed: false };
94
+
95
+ static getDerivedStateFromError() {
96
+ return { hasFailed: true };
97
+ }
98
+
99
+ override componentDidCatch(error: unknown) {
100
+ this.props.onError?.(error);
101
+ }
102
+
103
+ override render() {
104
+ return this.state.hasFailed ? null : this.props.children;
105
+ }
106
+ }
107
+
108
+ export function createRudraRecommendationsComponent({
109
+ loadRecommendations,
110
+ placeholder = null,
111
+ onError,
112
+ ...blockProps
113
+ }: RudraRecommendationsOptions): ReactComponentImplementation {
114
+ return createComponentImplementation(
115
+ rudraRecommendationsApi,
116
+ function RudraRecommendations({ props, context }) {
117
+ const written = useWrittenProperties(context.componentModel);
118
+ const request = pickRecommendationRequest(written, props);
119
+ const state = useRecommendationResult(request, loadRecommendations, onError);
120
+
121
+ if (state.status === 'loading') return placeholder;
122
+ if (state.status === 'failed') return null;
123
+ return (
124
+ <RenderFailureBoundary key={state.requestKey} onError={onError}>
125
+ <RudraComponent {...blockProps} {...state.result} />
126
+ </RenderFailureBoundary>
127
+ );
128
+ },
129
+ );
130
+ }
package/src/server.ts ADDED
@@ -0,0 +1,12 @@
1
+ export {
2
+ RUDRA_RECOMMENDATIONS_INSTRUCTIONS,
3
+ rudraRecommendationsApi,
4
+ } from './recommendations-api.js';
5
+
6
+ export {
7
+ fitRecommendationRequest,
8
+ parseRecommendationRequest,
9
+ type RecommendationRequest,
10
+ } from './recommendation-request.js';
11
+
12
+ export type { RecommendationResult } from './recommendation-result.js';