@rudra-js/a2ui 0.0.0-stage → 0.8.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/LICENSE +21 -0
- package/README.md +192 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/recommendation-request.d.ts +11 -0
- package/dist/recommendation-request.d.ts.map +1 -0
- package/dist/recommendation-request.js +34 -0
- package/dist/recommendation-request.js.map +1 -0
- package/dist/recommendation-result.d.ts +8 -0
- package/dist/recommendation-result.d.ts.map +1 -0
- package/dist/recommendation-result.js +13 -0
- package/dist/recommendation-result.js.map +1 -0
- package/dist/recommendations-api.d.ts +62 -0
- package/dist/recommendations-api.d.ts.map +1 -0
- package/dist/recommendations-api.js +20 -0
- package/dist/recommendations-api.js.map +1 -0
- package/dist/recommendations-component.d.ts +13 -0
- package/dist/recommendations-component.d.ts.map +1 -0
- package/dist/recommendations-component.js +73 -0
- package/dist/recommendations-component.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -0
- package/package.json +78 -4
- package/src/index.ts +8 -0
- package/src/recommendation-request.ts +48 -0
- package/src/recommendation-result.ts +20 -0
- package/src/recommendations-api.ts +31 -0
- package/src/recommendations-component.tsx +130 -0
- package/src/server.ts +12 -0
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
CHANGED
|
@@ -1,3 +1,193 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rudra-js/a2ui
|
|
2
2
|
|
|
3
|
-
This
|
|
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 @rudra-js/core @rudra-js/react @rudra-js/attested @a2ui/react @a2ui/web_core react react-dom
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Everything but zod is a peer dependency, so your app holds one copy of each. On pnpm, read
|
|
23
|
+
[zod](#zod) first.
|
|
24
|
+
|
|
25
|
+
Keep `skipLibCheck: true`. Without it, A2UI's own declarations fail to typecheck: they need `"dom"`
|
|
26
|
+
in `lib`, and two of their imports have no file extension, which `nodenext` refuses.
|
|
27
|
+
|
|
28
|
+
## Three pieces
|
|
29
|
+
|
|
30
|
+
### The catalog your agent is prompted with
|
|
31
|
+
|
|
32
|
+
Put `agentCatalog.toJson()` in your agent's prompt and have it create surfaces with
|
|
33
|
+
`catalogId: agentCatalog.id`. If you have catalog instructions of your own, join them to
|
|
34
|
+
`RUDRA_RECOMMENDATIONS_INSTRUCTIONS`.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Catalog } from '@a2ui/web_core/v0_9';
|
|
38
|
+
import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
|
|
39
|
+
import { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi } from '@rudra-js/a2ui/server';
|
|
40
|
+
|
|
41
|
+
export const agentCatalog = new Catalog(
|
|
42
|
+
'https://shop.example/a2ui/catalog.json',
|
|
43
|
+
'v0.9',
|
|
44
|
+
[...basicCatalog.components.values(), rudraRecommendationsApi],
|
|
45
|
+
[...basicCatalog.functions.values()],
|
|
46
|
+
{ instructions: RUDRA_RECOMMENDATIONS_INSTRUCTIONS },
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The first `toJson()` logs one "Recursive reference detected" warning per hint. It's harmless.
|
|
51
|
+
|
|
52
|
+
### Your endpoint
|
|
53
|
+
|
|
54
|
+
`shopperFor` and `candidatesFor` stand for the session and catalog code you already have.
|
|
55
|
+
`provider: null` calls no model, so pass your own provider. In the default cohort mode the model
|
|
56
|
+
never sees `searchQuery`, so the agent's search only counts with `generation: 'per-shopper'`.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createComponentGenerator, parseTrackingInput } from '@rudra-js/core';
|
|
60
|
+
import {
|
|
61
|
+
fitRecommendationRequest,
|
|
62
|
+
parseRecommendationRequest,
|
|
63
|
+
type RecommendationRequest,
|
|
64
|
+
type RecommendationResult,
|
|
65
|
+
} from '@rudra-js/a2ui/server';
|
|
66
|
+
import { candidatesFor, shopperFor } from './your-shop.js';
|
|
67
|
+
|
|
68
|
+
const generator = createComponentGenerator({ provider: null });
|
|
69
|
+
|
|
70
|
+
export async function POST(request: Request): Promise<Response> {
|
|
71
|
+
let asked: RecommendationRequest;
|
|
72
|
+
try {
|
|
73
|
+
asked = parseRecommendationRequest(await request.json());
|
|
74
|
+
} catch {
|
|
75
|
+
return new Response('Bad request', { status: 400 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const shopper = await shopperFor(request);
|
|
79
|
+
const candidates = await candidatesFor(shopper);
|
|
80
|
+
const hints = fitRecommendationRequest(asked, candidates);
|
|
81
|
+
const input = parseTrackingInput({
|
|
82
|
+
user: { id: shopper.id },
|
|
83
|
+
context: { surface: 'assistant', locale: shopper.locale, ...hints },
|
|
84
|
+
candidates,
|
|
85
|
+
signals: shopper.signals,
|
|
86
|
+
});
|
|
87
|
+
const spec = await generator.generate(input);
|
|
88
|
+
|
|
89
|
+
const result: RecommendationResult = { spec, products: input.candidates, bundles: input.bundles };
|
|
90
|
+
return Response.json(result);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `parseRecommendationRequest` accepts only `currentSku`, `currentCategory`, `searchQuery` and
|
|
95
|
+
`maxItems`. Anything else is a 400.
|
|
96
|
+
- `fitRecommendationRequest` drops a SKU or category none of your candidates has.
|
|
97
|
+
- Pick candidates on your side, not from the hints. Each new category, `maxItems` or candidate set
|
|
98
|
+
is a new model call, and with `per-shopper` so is each shopper and each new SKU or search, so
|
|
99
|
+
rate-limit the endpoint (see [SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md)).
|
|
100
|
+
- Send back `input.candidates` and `input.bundles`, not your raw catalog.
|
|
101
|
+
|
|
102
|
+
### The browser
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import type { ReactCatalogComponent } from '@a2ui/react/v0_9';
|
|
106
|
+
import { Catalog, MessageProcessor } from '@a2ui/web_core/v0_9';
|
|
107
|
+
import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
|
|
108
|
+
import { createRudraRecommendationsComponent } from '@rudra-js/a2ui';
|
|
109
|
+
|
|
110
|
+
const recommendations = createRudraRecommendationsComponent({
|
|
111
|
+
async loadRecommendations(request, signal) {
|
|
112
|
+
const response = await fetch('/api/recommendations', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'content-type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(request),
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) throw new Error(`recommendations: HTTP ${response.status}`);
|
|
119
|
+
return response.json();
|
|
120
|
+
},
|
|
121
|
+
locale: 'en-US',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const shopCatalog = new Catalog<ReactCatalogComponent>(
|
|
125
|
+
'https://shop.example/a2ui/catalog.json',
|
|
126
|
+
'v0.9',
|
|
127
|
+
[...basicCatalog.components.values(), recommendations],
|
|
128
|
+
[...basicCatalog.functions.values()],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export const processor = new MessageProcessor<ReactCatalogComponent>([shopCatalog]);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Give the browser catalog the same id as the agent catalog, because A2UI looks it up by the
|
|
135
|
+
`catalogId` the agent sends. Render the processor's surfaces with `A2uiSurface`. The options take
|
|
136
|
+
`loadRecommendations`, `placeholder`, `onError`, and every `RudraComponent` prop except `spec`,
|
|
137
|
+
`products` and `bundles`.
|
|
138
|
+
|
|
139
|
+
## What the component does
|
|
140
|
+
|
|
141
|
+
- It reads each hint as a literal or from the agent's data model (`{ "path": "/viewing/sku" }`),
|
|
142
|
+
and sends only the four. It leaves out a blank one, and one the server would turn away, like a
|
|
143
|
+
`maxItems` of 40. A literal of the wrong type, like a numeric SKU, makes A2UI refuse the agent's
|
|
144
|
+
whole message.
|
|
145
|
+
- It asks once the hints hold still for 300 ms, and keeps the last block up until the next one
|
|
146
|
+
arrives. `signal` aborts when the hints change or the component unmounts.
|
|
147
|
+
- It checks the result against core's schemas before rendering any of it.
|
|
148
|
+
- It shows `placeholder` until the first load. On a failed load, a bad result or a render error it
|
|
149
|
+
shows nothing and calls `onError`.
|
|
150
|
+
- Each component on a surface sends its own request.
|
|
151
|
+
|
|
152
|
+
## Where it runs
|
|
153
|
+
|
|
154
|
+
`@rudra-js/a2ui` holds `createRudraRecommendationsComponent` and its types, and loads only in the
|
|
155
|
+
browser, because `@a2ui/react` needs a DOM. The agent catalog and your endpoint import from
|
|
156
|
+
`@rudra-js/a2ui/server`, which runs anywhere. In Next.js a `'use client'` file still runs on the
|
|
157
|
+
server for the first render, so load the one that imports `@rudra-js/a2ui` from a client component
|
|
158
|
+
with `next/dynamic` and `ssr: false`.
|
|
159
|
+
|
|
160
|
+
A2UI renders on the client, so a crawler won't see a block an agent places. For a product page,
|
|
161
|
+
render `RudraComponent` on the server, as the
|
|
162
|
+
[`@rudra-js/react` README](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/react)
|
|
163
|
+
describes.
|
|
164
|
+
|
|
165
|
+
## zod
|
|
166
|
+
|
|
167
|
+
A2UI's schemas are zod 3, so this package depends on zod 3 while the rest of rudra-js uses zod 4.
|
|
168
|
+
npm installs both, each where its package finds it, and you don't need either one yourself.
|
|
169
|
+
|
|
170
|
+
On pnpm, `@a2ui/react` gets core's zod 4 as its zod peer, and the browser example stops
|
|
171
|
+
typechecking (TS2344). If your app has no zod of its own, add zod 3:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm add zod@^3.25.76
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If your app uses zod 4, drop the peer from `@a2ui/react` in `.pnpmfile.cjs`, and pnpm gives it
|
|
178
|
+
the zod 3 it depends on:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
module.exports = {
|
|
182
|
+
hooks: {
|
|
183
|
+
readPackage(manifest) {
|
|
184
|
+
if (manifest.name === '@a2ui/react') delete manifest.peerDependencies?.zod;
|
|
185
|
+
return manifest;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Licence
|
|
192
|
+
|
|
193
|
+
[MIT](./LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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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"}
|
package/dist/server.d.ts
ADDED
|
@@ -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 @@
|
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.8.1",
|
|
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.1",
|
|
66
|
+
"@rudra-js/react": "^0.8.1",
|
|
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.1",
|
|
73
|
+
"@rudra-js/react": "0.8.1",
|
|
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
|
+
}
|
|
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';
|