@web-applets/sdk 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -20
- package/dist/client.d.ts +12 -4
- package/dist/client.js +95 -56
- package/dist/context.d.ts +8 -5
- package/dist/context.js +44 -11
- package/dist/types.d.ts +24 -12
- package/dist/utils.d.ts +16 -1
- package/dist/utils.js +15 -0
- package/dist/web-component/index.d.ts +1 -0
- package/dist/web-component/index.js +52 -0
- package/dist/web-components/applet-frame.d.ts +1 -0
- package/dist/web-components/applet-frame.js +52 -0
- package/package.json +2 -1
package/README.md
CHANGED
@@ -2,23 +2,41 @@
|
|
2
2
|
|
3
3
|
> An open SDK to create interoperable actions & views for agents – _a web of capabilities!_
|
4
4
|
|
5
|
+
🔗 [Community Applets Repo](https://github.com/unternet-co/community-applets) | 🔗 [Community Mailing List](https://groups.google.com/a/unternet.co/g/community)
|
6
|
+
|
5
7
|
## What is it?
|
6
8
|
|
7
9
|
Web Applets is a specification for modular, local web software that can be read and used by both humans and machines. Web Applets aims to be an interoperabe application layer for agents – instead of chatbots that can only interact in plain text, Web Applets allow them to actuate real software, read the results, and render rich, graphical views in response.
|
8
10
|
|
11
|
+
In short, your model can use & respond with artibrary bits of web UI that a human can then interact with – a map, a rich text document, an item on Amazon with a purchase button, whatever you can build with the web you can turn it into an applet that an agent can use and display.
|
12
|
+
|
9
13
|
Did we mention it's _interoperable_? We think the future of software should be open & collaborative, not locked down to a single platform.
|
10
14
|
|
15
|
+
For more, see [why](./docs/why.md).
|
16
|
+
|
11
17
|
## Getting started
|
12
18
|
|
13
|
-
|
19
|
+
Install the applets SDK & CLI:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
npm i --save @web-applets/sdk
|
23
|
+
npm i --save-dev @web-applets/cli
|
24
|
+
```
|
25
|
+
|
26
|
+
Then, initialize the `applets.config.json` and create a new blank applet:
|
14
27
|
|
15
|
-
|
28
|
+
```bash
|
29
|
+
npx applets init
|
30
|
+
npx applets create <your-applet-name>
|
31
|
+
```
|
32
|
+
|
33
|
+
This creates an applet folder, with a build system built-in using Vite. You can change this to anything you want. We recommend building at this stage, as the SDK currently needs to be bundled. We're working on adding a statically hosted script to import.
|
16
34
|
|
17
35
|
Inside your applet folder, you'll find a basic web app setup:
|
18
36
|
|
19
37
|
- `public/manifest.json`: This file describes the Applet, and tells the model what actions are available and what parameters each action takes
|
20
38
|
- `index.html`: Much like a website, this holds the main page for your applet
|
21
|
-
- `main.
|
39
|
+
- `src/main.ts`: Declares functions that respond to each action, and a render function that updates the view based on state
|
22
40
|
|
23
41
|
> Want to use React? Svelte? Vue? – No problem, just install the dependencies and create an app the way you normally would in a website. So long as you're receiving the action events, it will all just work.
|
24
42
|
|
@@ -42,7 +60,7 @@ Let's say we want our applet to respond to a "set_name" action and render the us
|
|
42
60
|
}
|
43
61
|
```
|
44
62
|
|
45
|
-
Now let's update `main.ts` to assign an action handler:
|
63
|
+
Now let's update `src/main.ts` to assign an action handler:
|
46
64
|
|
47
65
|
```js
|
48
66
|
// First, import the SDK
|
@@ -69,44 +87,42 @@ applet.onrender = () => {
|
|
69
87
|
};
|
70
88
|
```
|
71
89
|
|
72
|
-
Now if you run `
|
90
|
+
Now if you run `npx applets playground`, you should be able to test out your new applet action directly. This applet will now work in any environment where the SDK is installed.
|
73
91
|
|
74
92
|

|
75
93
|
|
76
94
|
## Integrating Web Applets into your client
|
77
95
|
|
78
|
-
|
96
|
+
Using Web Applets is just as easy as creating them!
|
97
|
+
|
98
|
+
First, build your applets. By default, this goes into a folder called `dist/`, but you'll likely want to change this in `applets.config.json` to point to wherever you're serving public files from. For example, in a Vite project, edit this to be `./public`.
|
99
|
+
|
100
|
+
Then, run:
|
79
101
|
|
80
102
|
```bash
|
81
|
-
|
103
|
+
npx applets build
|
82
104
|
```
|
83
105
|
|
84
|
-
|
106
|
+
Now in your main app, you can import the applets client:
|
85
107
|
|
86
108
|
```js
|
87
|
-
import { applets } from '@
|
109
|
+
import { applets } from '@web-applets/sdk';
|
88
110
|
```
|
89
111
|
|
90
|
-
Now you can
|
112
|
+
Now you can import your applets from wherever they're being served from (note – you can also host them anywhere on the web):
|
91
113
|
|
92
114
|
```js
|
93
|
-
const applet = await applets.load(
|
94
|
-
`https://unternet.co/applets/helloworld.applet`
|
95
|
-
);
|
115
|
+
const applet = await applets.load('/helloworld.applet'); // replace with a URL if hosted remotely
|
96
116
|
applet.onstateupdated = (state) => console.log(state);
|
97
117
|
applet.dispatchAction('set_name', { name: 'Web Applets' });
|
98
|
-
// console.log: { name: "Web Applets" }
|
99
118
|
```
|
100
119
|
|
101
|
-
The above applet is actually running headless, but we can get it to display by attaching it to
|
120
|
+
The above applet is actually running headless, but we can get it to display by attaching it to a container. For the loading step, instead run:
|
102
121
|
|
103
122
|
```js
|
104
123
|
const container = document.createElement('iframe');
|
105
124
|
document.body.appendChild(container);
|
106
|
-
const applet = await applets.load(
|
107
|
-
`https://unternet.co/applets/helloworld.applet`,
|
108
|
-
container
|
109
|
-
);
|
125
|
+
const applet = await applets.load(`/helloworld.applet`, container);
|
110
126
|
```
|
111
127
|
|
112
128
|
To load pre-existing saved state into an applet, simply set the state property:
|
@@ -116,7 +132,7 @@ applet.state = { name: 'Ada Lovelace' };
|
|
116
132
|
// console.log: { name: "Ada Lovelace" }
|
117
133
|
```
|
118
134
|
|
119
|
-
It may also be helpful to check available applets at a domain, or in your
|
135
|
+
It may also be helpful to check available applets at a domain, or in your public folder. For that you can extract the applet headers from the App Manifest at the public root (`/manifest.json`), and see the available applets and a shorthand for the actions you can take in them. This is automatically created when you build your applets.
|
120
136
|
|
121
137
|
```js
|
122
138
|
const headers = await applets.getHeaders('/');
|
@@ -148,6 +164,10 @@ You can use it to present a quick summary of available tools to your model, and
|
|
148
164
|
|
149
165
|
## Feedback & Community
|
150
166
|
|
167
|
+
This is a community project, and we're open to community members discussing the project direction, and submitting code!
|
168
|
+
|
169
|
+
To join the conversation, visit the Applets mailing list at [groups.google.com/a/unternet.co/g/community](https://groups.google.com/a/unternet.co/g/community). You can also find more about the company that's kicking off this work at [unternet.co](https://unternet.co)
|
170
|
+
|
151
171
|
## License
|
152
172
|
|
153
173
|
[MIT](./LICENSE.md)
|
package/dist/client.d.ts
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest } from './types';
|
2
|
-
export declare function
|
3
|
-
|
4
|
-
|
1
|
+
import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest, AppletMessageType, AppletMessageCallback, AppletManifestDict } from './types';
|
2
|
+
export declare function list(url: string): Promise<AppletManifestDict>;
|
3
|
+
interface AppletOpts {
|
4
|
+
headless?: boolean;
|
5
|
+
unsafe?: boolean;
|
6
|
+
}
|
7
|
+
export declare function load(url: string, container?: HTMLIFrameElement, opts?: AppletOpts): Promise<Applet>;
|
5
8
|
export declare class Applet<T = unknown> extends EventTarget {
|
6
9
|
#private;
|
7
10
|
actions: AppletAction[];
|
@@ -20,5 +23,10 @@ export declare class Applet<T = unknown> extends EventTarget {
|
|
20
23
|
onstateupdated(event: CustomEvent): void;
|
21
24
|
disconnect(): void;
|
22
25
|
dispatchAction(actionId: string, params: ActionParams): Promise<AppletMessage<any>>;
|
26
|
+
send(message: AppletMessage): Promise<AppletMessage<any>>;
|
27
|
+
on(messageType: AppletMessageType, callback: AppletMessageCallback): Promise<void>;
|
23
28
|
}
|
24
29
|
export declare function loadManifest(url: string): Promise<AppletManifest>;
|
30
|
+
export declare function getHeaders(url: string): Promise<AppletHeader[]>;
|
31
|
+
export declare function getManifests(url: string): Promise<any[]>;
|
32
|
+
export {};
|
package/dist/client.js
CHANGED
@@ -9,73 +9,72 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
11
|
};
|
12
|
-
var _Applet_state;
|
12
|
+
var _Applet_instances, _Applet_state, _Applet_dispatchEvent;
|
13
13
|
import { AppletMessage, } from './types';
|
14
|
-
const
|
15
|
-
|
16
|
-
document.body.appendChild(
|
17
|
-
export async function
|
14
|
+
const hiddenContainer = document.createElement('iframe');
|
15
|
+
hiddenContainer.style.display = 'none';
|
16
|
+
document.body.appendChild(hiddenContainer);
|
17
|
+
export async function list(url) {
|
18
18
|
url = parseUrl(url);
|
19
19
|
try {
|
20
20
|
const request = await fetch(`${url}/manifest.json`);
|
21
21
|
const appManifest = await request.json();
|
22
|
-
const
|
23
|
-
|
22
|
+
const appletUrls = appManifest.applets;
|
23
|
+
const manifests = {};
|
24
|
+
const manifestRequests = appletUrls.map(async (appletUrl) => {
|
25
|
+
appletUrl = parseUrl(appletUrl, url);
|
26
|
+
const request = await fetch(`${appletUrl}/manifest.json`);
|
27
|
+
const manifest = await request.json();
|
28
|
+
manifests[appletUrl] = manifest;
|
29
|
+
});
|
30
|
+
await Promise.all(manifestRequests);
|
31
|
+
return manifests;
|
24
32
|
}
|
25
33
|
catch {
|
26
|
-
return
|
34
|
+
return {};
|
27
35
|
}
|
28
36
|
}
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
const defaultOpts = {
|
38
|
+
headless: false,
|
39
|
+
unsafe: false,
|
40
|
+
};
|
41
|
+
export async function load(url, container, opts) {
|
42
|
+
const _opts = Object.assign(defaultOpts, opts ?? {});
|
43
|
+
if (!container) {
|
44
|
+
container = hiddenContainer;
|
45
|
+
_opts.headless = true;
|
46
|
+
}
|
47
|
+
if (!_opts.unsafe)
|
48
|
+
container.setAttribute('sandbox', 'allow-scripts allow-forms');
|
41
49
|
url = parseUrl(url);
|
42
50
|
const manifest = await loadManifest(`${url}`);
|
43
51
|
const applet = new Applet();
|
44
52
|
applet.manifest = manifest;
|
45
|
-
applet.actions = manifest.actions;
|
53
|
+
applet.actions = manifest.actions;
|
46
54
|
applet.container = container;
|
47
55
|
container.src = applet.manifest.entrypoint;
|
48
|
-
if (!container.isConnected)
|
49
|
-
hiddenRoot.appendChild(container);
|
50
56
|
return new Promise((resolve) => {
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
57
|
+
applet.on('ready', () => {
|
58
|
+
const initMessage = new AppletMessage('init', {
|
59
|
+
headless: _opts.headless,
|
60
|
+
});
|
61
|
+
applet.send(initMessage);
|
62
|
+
resolve(applet);
|
56
63
|
});
|
57
64
|
});
|
58
65
|
}
|
59
66
|
export class Applet extends EventTarget {
|
60
67
|
constructor() {
|
61
68
|
super();
|
69
|
+
_Applet_instances.add(this);
|
62
70
|
this.actions = [];
|
63
71
|
_Applet_state.set(this, void 0);
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
this.onstateupdated(message.data.state);
|
71
|
-
}
|
72
|
-
if (message.data.type === 'resize') {
|
73
|
-
this.resizeContainer(message.data.dimensions);
|
74
|
-
}
|
75
|
-
this.container.contentWindow?.postMessage({
|
76
|
-
type: 'resolve',
|
77
|
-
id: message.data.id,
|
78
|
-
}, '*');
|
72
|
+
this.on('state', (message) => {
|
73
|
+
__classPrivateFieldSet(this, _Applet_state, message.state, "f");
|
74
|
+
__classPrivateFieldGet(this, _Applet_instances, "m", _Applet_dispatchEvent).call(this, 'stateupdated', message.state);
|
75
|
+
});
|
76
|
+
this.on('resize', (message) => {
|
77
|
+
this.resizeContainer(message.dimensions);
|
79
78
|
});
|
80
79
|
}
|
81
80
|
get state() {
|
@@ -83,8 +82,7 @@ export class Applet extends EventTarget {
|
|
83
82
|
}
|
84
83
|
set state(state) {
|
85
84
|
__classPrivateFieldSet(this, _Applet_state, state, "f");
|
86
|
-
|
87
|
-
this.container.contentWindow?.postMessage(stateMessage.toJson(), '*');
|
85
|
+
this.send(new AppletMessage('state', { state }));
|
88
86
|
}
|
89
87
|
toJson() {
|
90
88
|
return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
|
@@ -98,7 +96,7 @@ export class Applet extends EventTarget {
|
|
98
96
|
}));
|
99
97
|
}
|
100
98
|
resizeContainer(dimensions) {
|
101
|
-
this.container.style.height = `${dimensions.height}px`;
|
99
|
+
this.container.style.height = `${dimensions.height + 2}px`;
|
102
100
|
// if (!this.#styleOverrides) {
|
103
101
|
// this.#container.style.height = `${dimensions.height}px`;
|
104
102
|
// }
|
@@ -110,14 +108,15 @@ export class Applet extends EventTarget {
|
|
110
108
|
actionId,
|
111
109
|
params,
|
112
110
|
});
|
113
|
-
this.
|
111
|
+
return await this.send(requestMessage);
|
112
|
+
}
|
113
|
+
async send(message) {
|
114
|
+
this.container.contentWindow?.postMessage(message.toJson(), '*');
|
114
115
|
return new Promise((resolve) => {
|
115
116
|
const listener = (messageEvent) => {
|
116
|
-
if (messageEvent.source !== this.container.contentWindow)
|
117
|
-
return;
|
118
117
|
const responseMessage = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
119
118
|
if (responseMessage.type === 'resolve' &&
|
120
|
-
responseMessage.id ===
|
119
|
+
responseMessage.id === message.id) {
|
121
120
|
window.removeEventListener('message', listener);
|
122
121
|
resolve(responseMessage);
|
123
122
|
}
|
@@ -125,20 +124,37 @@ export class Applet extends EventTarget {
|
|
125
124
|
window.addEventListener('message', listener);
|
126
125
|
});
|
127
126
|
}
|
127
|
+
async on(messageType, callback) {
|
128
|
+
const listener = async (messageEvent) => {
|
129
|
+
if (messageEvent.source !== this.container.contentWindow)
|
130
|
+
return;
|
131
|
+
if (messageEvent.data.type !== messageType)
|
132
|
+
return;
|
133
|
+
const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
134
|
+
await callback(message);
|
135
|
+
this.container.contentWindow?.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
|
136
|
+
};
|
137
|
+
window.addEventListener('message', listener);
|
138
|
+
}
|
128
139
|
}
|
129
|
-
_Applet_state = new WeakMap()
|
140
|
+
_Applet_state = new WeakMap(), _Applet_instances = new WeakSet(), _Applet_dispatchEvent = function _Applet_dispatchEvent(id, detail) {
|
141
|
+
if (typeof this[`on${id}`] === 'function') {
|
142
|
+
this[`on${id}`](detail);
|
143
|
+
}
|
144
|
+
this.dispatchEvent(new CustomEvent(id, { detail }));
|
145
|
+
};
|
146
|
+
/* Helpers */
|
130
147
|
function parseUrl(url, base) {
|
131
148
|
if (['http', 'https'].includes(url.split('://')[0])) {
|
132
149
|
return url;
|
133
150
|
}
|
134
|
-
let path = url;
|
135
|
-
if (path.startsWith('/'))
|
136
|
-
path = path.slice(1);
|
137
|
-
if (path.endsWith('/'))
|
138
|
-
path = path.slice(0, -1);
|
151
|
+
let path = trimSlashes(url);
|
139
152
|
url = `${base || window.location.origin}/${path}`;
|
140
153
|
return url;
|
141
154
|
}
|
155
|
+
function trimSlashes(str) {
|
156
|
+
return str.replace(/^\/+|\/+$/g, '');
|
157
|
+
}
|
142
158
|
export async function loadManifest(url) {
|
143
159
|
url = parseUrl(url);
|
144
160
|
const request = await fetch(`${url}/manifest.json`);
|
@@ -149,3 +165,26 @@ export async function loadManifest(url) {
|
|
149
165
|
appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
150
166
|
return appletManifest;
|
151
167
|
}
|
168
|
+
export async function getHeaders(url) {
|
169
|
+
url = parseUrl(url);
|
170
|
+
try {
|
171
|
+
const request = await fetch(`${url}/manifest.json`);
|
172
|
+
const appManifest = await request.json();
|
173
|
+
const appletHeaders = appManifest.applets;
|
174
|
+
return appletHeaders ?? [];
|
175
|
+
}
|
176
|
+
catch {
|
177
|
+
return [];
|
178
|
+
}
|
179
|
+
}
|
180
|
+
export async function getManifests(url) {
|
181
|
+
url = parseUrl(url);
|
182
|
+
const request = await fetch(`${url}/manifest.json`);
|
183
|
+
const headers = (await request.json()).applets;
|
184
|
+
const manifests = await Promise.all(headers.map(async (header) => {
|
185
|
+
const appletUrl = parseUrl(header.url);
|
186
|
+
const request = await fetch(`${appletUrl}/manifest.json`);
|
187
|
+
return await request.json();
|
188
|
+
}));
|
189
|
+
return manifests ?? [];
|
190
|
+
}
|
package/dist/context.d.ts
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
-
import { ActionHandlerDict,
|
1
|
+
import { ActionHandlerDict, AppletMessage, AppletMessageType, AppletMessageCallback, ActionParams, ActionHandler } from './types';
|
2
2
|
/**
|
3
3
|
* Context
|
4
4
|
*/
|
5
|
-
export declare class AppletContext<StateType =
|
5
|
+
export declare class AppletContext<StateType = any> extends EventTarget {
|
6
|
+
#private;
|
6
7
|
client: AppletClient;
|
7
8
|
actionHandlers: ActionHandlerDict;
|
8
|
-
|
9
|
+
headless: boolean;
|
9
10
|
connect(): this;
|
10
11
|
setActionHandler<T extends ActionParams>(actionId: string, handler: ActionHandler<T>): void;
|
11
|
-
|
12
|
+
set state(state: StateType);
|
13
|
+
get state(): StateType;
|
14
|
+
setState(state: StateType, shouldRender?: boolean): Promise<void>;
|
12
15
|
onload(): Promise<void> | void;
|
13
16
|
onready(): Promise<void> | void;
|
14
17
|
onrender(): void;
|
@@ -20,5 +23,5 @@ declare class AppletClient {
|
|
20
23
|
on(messageType: AppletMessageType, callback: AppletMessageCallback): void;
|
21
24
|
send(message: AppletMessage): Promise<void>;
|
22
25
|
}
|
23
|
-
export declare const appletContext: AppletContext<
|
26
|
+
export declare const appletContext: AppletContext<any>;
|
24
27
|
export {};
|
package/dist/context.js
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
5
|
+
};
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
11
|
+
};
|
12
|
+
var _AppletContext_state;
|
1
13
|
import { AppletMessage, } from './types';
|
2
14
|
/**
|
3
15
|
* Context
|
@@ -6,6 +18,8 @@ export class AppletContext extends EventTarget {
|
|
6
18
|
constructor() {
|
7
19
|
super(...arguments);
|
8
20
|
this.actionHandlers = {};
|
21
|
+
_AppletContext_state.set(this, void 0);
|
22
|
+
this.headless = false;
|
9
23
|
}
|
10
24
|
connect() {
|
11
25
|
this.client = new AppletClient();
|
@@ -34,11 +48,24 @@ export class AppletContext extends EventTarget {
|
|
34
48
|
}
|
35
49
|
});
|
36
50
|
resizeObserver.observe(document.querySelector('html'));
|
51
|
+
this.client.on('init', (message) => {
|
52
|
+
const initMessage = message;
|
53
|
+
this.headless = initMessage.headless;
|
54
|
+
});
|
37
55
|
this.client.on('state', (message) => {
|
38
56
|
if (!isStateMessage(message)) {
|
39
57
|
throw new TypeError("Message doesn't match type StateMessage");
|
40
58
|
}
|
41
|
-
|
59
|
+
// Don't render when state updates match the current state
|
60
|
+
// this retains cursor positions in text fields, for example
|
61
|
+
if (JSON.stringify(message.state) === JSON.stringify(__classPrivateFieldGet(this, _AppletContext_state, "f")))
|
62
|
+
return;
|
63
|
+
__classPrivateFieldSet(this, _AppletContext_state, message.state, "f");
|
64
|
+
// BUG: For some reason regular applets were loading headless, when instantiated not on a page reload
|
65
|
+
// if (!this.headless) {
|
66
|
+
this.onrender();
|
67
|
+
this.dispatchEvent(new CustomEvent('render'));
|
68
|
+
// }
|
42
69
|
});
|
43
70
|
this.client.on('action', async (message) => {
|
44
71
|
if (!isActionMessage(message)) {
|
@@ -47,24 +74,32 @@ export class AppletContext extends EventTarget {
|
|
47
74
|
if (Object.keys(this.actionHandlers).includes(message.actionId)) {
|
48
75
|
await this.actionHandlers[message.actionId](message.params);
|
49
76
|
}
|
50
|
-
message.resolve();
|
51
77
|
});
|
52
78
|
return this;
|
53
79
|
}
|
54
80
|
setActionHandler(actionId, handler) {
|
55
81
|
this.actionHandlers[actionId] = handler;
|
56
82
|
}
|
57
|
-
|
83
|
+
set state(state) {
|
84
|
+
this.setState(state);
|
85
|
+
}
|
86
|
+
get state() {
|
87
|
+
return __classPrivateFieldGet(this, _AppletContext_state, "f");
|
88
|
+
}
|
89
|
+
async setState(state, shouldRender) {
|
58
90
|
const message = new AppletMessage('state', { state });
|
59
91
|
await this.client.send(message);
|
60
|
-
this
|
61
|
-
this.
|
62
|
-
|
92
|
+
__classPrivateFieldSet(this, _AppletContext_state, state, "f");
|
93
|
+
if (shouldRender !== false && !this.headless) {
|
94
|
+
this.onrender();
|
95
|
+
this.dispatchEvent(new CustomEvent('render'));
|
96
|
+
}
|
63
97
|
}
|
64
98
|
onload() { }
|
65
99
|
onready() { }
|
66
100
|
onrender() { }
|
67
101
|
}
|
102
|
+
_AppletContext_state = new WeakMap();
|
68
103
|
function isActionMessage(message) {
|
69
104
|
return message.type === 'action';
|
70
105
|
}
|
@@ -76,14 +111,12 @@ function isStateMessage(message) {
|
|
76
111
|
*/
|
77
112
|
class AppletClient {
|
78
113
|
on(messageType, callback) {
|
79
|
-
window.addEventListener('message', (messageEvent) => {
|
114
|
+
window.addEventListener('message', async (messageEvent) => {
|
80
115
|
if (messageEvent.data.type !== messageType)
|
81
116
|
return;
|
82
117
|
const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
83
|
-
|
84
|
-
|
85
|
-
};
|
86
|
-
callback(message);
|
118
|
+
await callback(message);
|
119
|
+
window.parent.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
|
87
120
|
});
|
88
121
|
}
|
89
122
|
send(message) {
|
package/dist/types.d.ts
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
export interface AppletManifest {
|
2
2
|
type: 'applet';
|
3
3
|
name: string;
|
4
|
-
description
|
4
|
+
description?: string;
|
5
5
|
icon?: string;
|
6
|
-
|
7
|
-
|
6
|
+
frameless?: boolean;
|
7
|
+
entrypoint?: string;
|
8
|
+
actions?: AppletAction[];
|
9
|
+
}
|
10
|
+
export interface AppletManifestDict {
|
11
|
+
[url: string]: AppletManifest;
|
8
12
|
}
|
9
13
|
export interface AppletAction {
|
10
14
|
id: string;
|
11
|
-
|
15
|
+
name?: string;
|
16
|
+
description?: string;
|
12
17
|
params?: ActionParamSchema;
|
13
18
|
}
|
14
19
|
export interface AppletHeader {
|
@@ -23,12 +28,12 @@ export interface AppletHeader {
|
|
23
28
|
};
|
24
29
|
}[];
|
25
30
|
}
|
26
|
-
export type AppletState =
|
31
|
+
export type AppletState = any;
|
27
32
|
export type ActionParamSchema = Record<string, {
|
28
33
|
description: string;
|
29
34
|
type: 'string';
|
30
35
|
}>;
|
31
|
-
export type ActionParams = Record<string,
|
36
|
+
export type ActionParams<T = any> = Record<string, T>;
|
32
37
|
export type ActionHandlerDict = {
|
33
38
|
[key: string]: ActionHandler<any>;
|
34
39
|
};
|
@@ -38,11 +43,22 @@ export interface AppletStateMessage<T = any> extends AppletMessage {
|
|
38
43
|
type: 'state';
|
39
44
|
state: T;
|
40
45
|
}
|
46
|
+
export interface AppletResizeMessage extends AppletMessage {
|
47
|
+
type: 'resize';
|
48
|
+
dimensions: {
|
49
|
+
height: number;
|
50
|
+
width: number;
|
51
|
+
};
|
52
|
+
}
|
41
53
|
export interface AppletActionMessage<T = any> extends AppletMessage {
|
42
54
|
type: 'action';
|
43
55
|
actionId: string;
|
44
56
|
params: T;
|
45
57
|
}
|
58
|
+
export interface AppletInitMessage extends AppletMessage {
|
59
|
+
type: 'init';
|
60
|
+
headless: boolean;
|
61
|
+
}
|
46
62
|
export declare class AppletMessage<T = any> {
|
47
63
|
type: AppletMessageType;
|
48
64
|
id: string;
|
@@ -53,9 +69,5 @@ export declare class AppletMessage<T = any> {
|
|
53
69
|
};
|
54
70
|
resolve(): void;
|
55
71
|
}
|
56
|
-
export type AppletMessageType = 'action' | 'render' | 'state' | 'ready' | 'resolve' | 'resize';
|
57
|
-
export type AppletMessageCallback = (message: AnyAppletMessage) => void;
|
58
|
-
type Serializable = string | number | boolean | null | Serializable[] | {
|
59
|
-
[key: string]: Serializable;
|
60
|
-
};
|
61
|
-
export {};
|
72
|
+
export type AppletMessageType = 'action' | 'actions' | 'render' | 'state' | 'init' | 'ready' | 'resolve' | 'resize';
|
73
|
+
export type AppletMessageCallback = (message: AnyAppletMessage) => Promise<void> | void;
|
package/dist/utils.d.ts
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
-
import { type AppletManifest } from './types';
|
1
|
+
import { AppletAction, type AppletManifest } from './types';
|
2
2
|
export declare function getAppletsList(url: string): Promise<any>;
|
3
3
|
export declare function loadAppletManifest(url: string): Promise<AppletManifest>;
|
4
|
+
export declare function createOpenAISchemaForAction(action: AppletAction): {
|
5
|
+
strict: boolean;
|
6
|
+
name: string;
|
7
|
+
schema: {
|
8
|
+
type: string;
|
9
|
+
required: string[];
|
10
|
+
properties: {
|
11
|
+
id: {
|
12
|
+
type: string;
|
13
|
+
};
|
14
|
+
params: import("./types").ActionParamSchema;
|
15
|
+
};
|
16
|
+
additionalProperties: boolean;
|
17
|
+
};
|
18
|
+
};
|
package/dist/utils.js
CHANGED
@@ -31,3 +31,18 @@ export async function loadAppletManifest(url) {
|
|
31
31
|
appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
32
32
|
return appletManifest;
|
33
33
|
}
|
34
|
+
export function createOpenAISchemaForAction(action) {
|
35
|
+
return {
|
36
|
+
strict: true,
|
37
|
+
name: 'action_schema',
|
38
|
+
schema: {
|
39
|
+
type: 'object',
|
40
|
+
required: Object.keys(action),
|
41
|
+
properties: {
|
42
|
+
id: { type: 'string' },
|
43
|
+
params: action.params,
|
44
|
+
},
|
45
|
+
additionalProperties: false,
|
46
|
+
},
|
47
|
+
};
|
48
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { applets } from '../index';
|
2
|
+
class AppletView extends HTMLElement {
|
3
|
+
connectedCallback() {
|
4
|
+
const styles = document.createElement('style');
|
5
|
+
styles.textContent = this.styles;
|
6
|
+
this.appendChild(styles);
|
7
|
+
this.container = document.createElement('iframe');
|
8
|
+
this.appendChild(this.container);
|
9
|
+
}
|
10
|
+
get styles() {
|
11
|
+
return /*css*/ `
|
12
|
+
applet-frame {
|
13
|
+
display: flex;
|
14
|
+
flex-direction: column;
|
15
|
+
}
|
16
|
+
|
17
|
+
applet-frame iframe {
|
18
|
+
border: none;
|
19
|
+
}
|
20
|
+
|
21
|
+
applet-frame:not(.frameless) {
|
22
|
+
border: 1px solid #ddd;
|
23
|
+
}
|
24
|
+
|
25
|
+
applet-frame.frameless {
|
26
|
+
padding: 0 7px;
|
27
|
+
}
|
28
|
+
`;
|
29
|
+
}
|
30
|
+
set url(url) {
|
31
|
+
setTimeout(() => this.loadApplet(url), 1);
|
32
|
+
}
|
33
|
+
async loadApplet(url) {
|
34
|
+
if (!this.container)
|
35
|
+
return;
|
36
|
+
this.applet = await applets.load(url, this.container);
|
37
|
+
if (this.applet.manifest.frameless)
|
38
|
+
this.classList.add('frameless');
|
39
|
+
this.applet.onstateupdated = () => {
|
40
|
+
this.dispatchEvent(new CustomEvent('stateupdated', { detail: this.applet.state }));
|
41
|
+
};
|
42
|
+
this.dispatchEvent(new CustomEvent('load'));
|
43
|
+
}
|
44
|
+
set state(state) {
|
45
|
+
if (this.applet)
|
46
|
+
this.applet.state = state;
|
47
|
+
this.addEventListener('load', () => {
|
48
|
+
this.applet.state = state;
|
49
|
+
});
|
50
|
+
}
|
51
|
+
}
|
52
|
+
customElements.define('applet-frame', AppletView);
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { applets } from '../index.js';
|
2
|
+
class AppletFrame extends HTMLElement {
|
3
|
+
connectedCallback() {
|
4
|
+
const styles = document.createElement('style');
|
5
|
+
styles.textContent = this.styles;
|
6
|
+
this.appendChild(styles);
|
7
|
+
this.container = document.createElement('iframe');
|
8
|
+
this.appendChild(this.container);
|
9
|
+
}
|
10
|
+
get styles() {
|
11
|
+
return /*css*/ `
|
12
|
+
applet-frame {
|
13
|
+
display: flex;
|
14
|
+
flex-direction: column;
|
15
|
+
}
|
16
|
+
|
17
|
+
applet-frame iframe {
|
18
|
+
border: none;
|
19
|
+
}
|
20
|
+
|
21
|
+
applet-frame:not(.frameless) {
|
22
|
+
border: 1px solid #ddd;
|
23
|
+
}
|
24
|
+
|
25
|
+
applet-frame.frameless {
|
26
|
+
padding: 0 7px;
|
27
|
+
}
|
28
|
+
`;
|
29
|
+
}
|
30
|
+
set url(url) {
|
31
|
+
setTimeout(() => this.loadApplet(url), 1);
|
32
|
+
}
|
33
|
+
async loadApplet(url) {
|
34
|
+
if (!this.container)
|
35
|
+
return;
|
36
|
+
this.applet = await applets.load(url, this.container);
|
37
|
+
if (this.applet.manifest.frameless)
|
38
|
+
this.classList.add('frameless');
|
39
|
+
this.applet.onstateupdated = () => {
|
40
|
+
this.dispatchEvent(new CustomEvent('stateupdated', { detail: this.applet.state }));
|
41
|
+
};
|
42
|
+
this.dispatchEvent(new CustomEvent('load'));
|
43
|
+
}
|
44
|
+
set state(state) {
|
45
|
+
if (this.applet)
|
46
|
+
this.applet.state = state;
|
47
|
+
this.addEventListener('load', () => {
|
48
|
+
this.applet.state = state;
|
49
|
+
});
|
50
|
+
}
|
51
|
+
}
|
52
|
+
customElements.define('applet-frame', AppletFrame);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@web-applets/sdk",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.8",
|
4
4
|
"description": "The Web Applets SDK, for creating & hosting Web Applets.",
|
5
5
|
"author": "Rupert Manfredi <rupert@unternet.co>",
|
6
6
|
"license": "MIT",
|
@@ -25,6 +25,7 @@
|
|
25
25
|
"typescript": "^5.6.2"
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
|
+
"marked": "^14.1.3",
|
28
29
|
"vite": "^5.4.7"
|
29
30
|
}
|
30
31
|
}
|