@web-applets/sdk 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +20 -19
- package/dist/core/applet.d.ts +2 -2
- package/dist/core/shared.d.ts +3 -4
- package/dist/core/shared.js +6 -2
- package/dist/utils.js +19 -3
- package/package.json +1 -1
- package/dist/core/host.d.ts +0 -31
- package/dist/core/host.js +0 -134
- package/dist/lib/utils.d.ts +0 -17
- package/dist/lib/utils.js +0 -37
package/README.md
CHANGED
@@ -2,34 +2,24 @@
|
|
2
2
|
|
3
3
|
> An open spec & SDK for creating apps that agents can use.
|
4
4
|
|
5
|
-
|
5
|
+
💌 [Mailing List](https://groups.google.com/a/unternet.co/g/community)
|
6
6
|
|
7
7
|
## What is it?
|
8
8
|
|
9
|
-
Web Applets is an open specification for building software that both humans and AI can understand and use together
|
9
|
+
**Web Applets is an open specification for building software that both humans and AI can understand and use together.** Instead of forcing AI to operate traditional point-and-click apps built for humans, Web Applets creates a new kind of software designed for human-AI collaboration.
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
Web Applets are modular pieces of web software that:
|
14
|
-
|
15
|
-
- **Can be used directly by humans with rich, graphical interfaces**
|
16
|
-
- **Can be understood and operated by AI through a clear protocol**
|
17
|
-
- **Run locally in your environment, not on distant servers**
|
18
|
-
- **Share context and state with their environment**
|
19
|
-
- **Can be freely combined and composed**
|
11
|
+
Think of them a bit like Claude artifacts, but they _do stuff_ & _work anywhere_!
|
20
12
|
|
21
|
-
|
13
|
+
![Demo of a web applets chatbot](./docs/assets/applets-chat-demo.gif)
|
22
14
|
|
23
|
-
|
15
|
+
Think of any web software you use today - maps, documents, shopping, calendars - and imagine if instead of visiting these as separate websites, you could pull them into your own environment where both you and AI could use them together seamlessly. Web applets can do that!
|
24
16
|
|
25
|
-
- **Built on Web Standards:** Create applets using familiar web technologies (HTML, CSS, JavaScript)
|
17
|
+
- **Built on Web Standards:** Create applets using familiar web technologies (HTML, CSS, JavaScript, React, Vue, etc.)
|
26
18
|
- **AI-Native Protocol:** Applets expose their state and actions in a way AI can understand and use
|
27
19
|
- **Rich Interfaces:** Full support for complex graphical UIs, not just text
|
28
20
|
- **Local-First:** Runs in your environment, keeping your data under your control
|
29
21
|
- **Composable:** Applets can work together, sharing context and state
|
30
|
-
- **Open Standard:** Designed for interoperability, not platform lock-in
|
31
|
-
|
32
|
-
Web Applets aims to do for AI-enabled software what the web did for documents - create an open platform where anyone can build, share, and connect applications. We believe the future of software should be built on open collaboration, not tight integration with closed platforms.
|
22
|
+
- **Open Standard:** Designed for interoperability across clients, not platform lock-in
|
33
23
|
|
34
24
|
## Example
|
35
25
|
|
@@ -75,6 +65,17 @@ context.ondata = () => {
|
|
75
65
|
|
76
66
|
Done! If you load this up in the inspector and introduce yourself, it will respond by greeting you.
|
77
67
|
|
68
|
+
To use this applet, we need to load it in our host web app using the SDK. Assuming the applet lives in our public directory, here's what that might look like:
|
69
|
+
|
70
|
+
```js
|
71
|
+
const applet = await applets.load('/helloworld.applet');
|
72
|
+
applet.onstateupdated = (state) => console.log(state);
|
73
|
+
applet.dispatchAction('set_name', { name: 'Web Applets' });
|
74
|
+
// { name: 'Web Applets' }
|
75
|
+
```
|
76
|
+
|
77
|
+
For a live example you can download and play with now, check out the [applets chat demo](https://github.com/unternet-co/applets-chat).
|
78
|
+
|
78
79
|
## Getting started
|
79
80
|
|
80
81
|
Create a new web app with the applets SDK installed. You can do this quickly using our CLI:
|
@@ -93,7 +94,7 @@ Inside the generated folder, you'll find a basic web app setup:
|
|
93
94
|
|
94
95
|
Now if you run `npx @web-applets/inspector`, you should be able to test out your new applet directly. This applet will now work in any environment where the SDK is installed.
|
95
96
|
|
96
|
-
![A screenshot showing the 'playground' editing UI, with a web applets showing 'Hello, Web Applets'](docs/assets/web-applets-
|
97
|
+
![A screenshot showing the 'playground' editing UI, with a web applets showing 'Hello, Web Applets'](docs/assets/web-applets-inspector.png)
|
97
98
|
|
98
99
|
## Integrating Web Applets into your client
|
99
100
|
|
@@ -136,7 +137,7 @@ applet.data = { name: 'Ada Lovelace' };
|
|
136
137
|
|
137
138
|
This is a community project, and we're open to community members discussing the project direction, and submitting code!
|
138
139
|
|
139
|
-
To join the conversation, visit the Applets mailing list
|
140
|
+
To join the conversation, visit the Applets mailing list [here](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)
|
140
141
|
|
141
142
|
## License
|
142
143
|
|
package/dist/core/applet.d.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AppletAction,
|
1
|
+
import { AppletAction, ActionParams, AppletManifest, AppletDataEvent, AppletResizeEvent, AppletActionsEvent, AppletMessageRelay, AppletReadyEvent } from './shared';
|
2
2
|
export declare function load(url: string, container?: HTMLIFrameElement): Promise<Applet>;
|
3
3
|
export interface AppletOptions {
|
4
4
|
manifest: AppletManifest;
|
@@ -10,7 +10,7 @@ export declare class Applet<T = any> extends EventTarget {
|
|
10
10
|
actions: AppletAction[];
|
11
11
|
container: HTMLIFrameElement;
|
12
12
|
constructor(manifest: AppletManifest, targetWindow: Window);
|
13
|
-
dispatchAction(actionId: string, params?: ActionParams): Promise<AppletMessage>;
|
13
|
+
dispatchAction(actionId: string, params?: ActionParams): Promise<import("./shared").AppletMessage>;
|
14
14
|
get data(): T;
|
15
15
|
set data(data: T);
|
16
16
|
get manifest(): AppletManifest;
|
package/dist/core/shared.d.ts
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
export interface AppletManifest {
|
2
2
|
name?: string;
|
3
3
|
short_name?: string;
|
4
|
-
icons:
|
4
|
+
icons: ManifestIcon[];
|
5
5
|
description?: string;
|
6
|
-
icon?: string;
|
7
6
|
display?: string;
|
8
7
|
start_url?: string;
|
9
8
|
unsafe?: boolean;
|
10
9
|
actions?: AppletAction[];
|
11
10
|
}
|
12
|
-
export interface
|
11
|
+
export interface ManifestIcon {
|
13
12
|
src: string;
|
14
13
|
purpose?: string;
|
15
14
|
sizes?: string;
|
@@ -91,7 +90,7 @@ export declare class AppletActionMessage extends AppletMessage {
|
|
91
90
|
export declare class AppletInitMessage extends AppletMessage {
|
92
91
|
constructor();
|
93
92
|
}
|
94
|
-
export type AppletMessageType = 'action' | 'actions' | 'data' | 'init' | 'ready' | 'resolve' | 'resize';
|
93
|
+
export type AppletMessageType = 'action' | 'actions' | 'data' | 'init' | 'ready' | 'style' | 'resolve' | 'resize';
|
95
94
|
export type AppletMessageCallback = (message: AppletMessage) => Promise<void> | void;
|
96
95
|
export declare class AppletDataEvent extends Event {
|
97
96
|
data: any;
|
package/dist/core/shared.js
CHANGED
@@ -9,10 +9,14 @@ export async function loadManifest(pageUrl) {
|
|
9
9
|
const parser = new DOMParser();
|
10
10
|
const doc = parser.parseFromString(html, 'text/html');
|
11
11
|
const linkElem = doc.querySelector('link[rel="manifest"]');
|
12
|
-
const
|
12
|
+
const href = linkElem.getAttribute('href');
|
13
|
+
const manifestUrl = parseUrl(href, pageUrl);
|
13
14
|
const manifestRequest = await fetch(manifestUrl);
|
14
15
|
manifest = await manifestRequest.json();
|
15
|
-
|
16
|
+
manifest.icons = manifest.icons.map((icon) => {
|
17
|
+
icon.src = parseUrl(icon.src, pageUrl);
|
18
|
+
return icon;
|
19
|
+
});
|
16
20
|
}
|
17
21
|
catch (e) {
|
18
22
|
return;
|
package/dist/utils.js
CHANGED
@@ -1,8 +1,24 @@
|
|
1
1
|
// Adds http/https to URLs, and prepends with window location if relative
|
2
2
|
export function parseUrl(url, base) {
|
3
|
-
if (url)
|
4
|
-
|
5
|
-
|
3
|
+
if (!url)
|
4
|
+
return '';
|
5
|
+
try {
|
6
|
+
// If the base URL is provided, ensure it has a trailing slash for proper path resolution
|
7
|
+
if (base) {
|
8
|
+
// Don't add trailing slash if the base already ends with a file extension
|
9
|
+
if (!base.match(/\.[a-zA-Z0-9]+$/)) {
|
10
|
+
base = base.endsWith('/') ? base : base + '/';
|
11
|
+
}
|
12
|
+
}
|
13
|
+
// Use URL constructor to properly resolve relative paths
|
14
|
+
const resolvedUrl = new URL(url, base ?? window.location.href);
|
15
|
+
return trimTrailingSlash(resolvedUrl.href);
|
16
|
+
}
|
17
|
+
catch (e) {
|
18
|
+
// Return original URL if parsing fails
|
19
|
+
console.warn('Failed to parse URL:', e);
|
20
|
+
return url;
|
21
|
+
}
|
6
22
|
}
|
7
23
|
function trimTrailingSlash(url) {
|
8
24
|
if (url.endsWith('/')) {
|
package/package.json
CHANGED
package/dist/core/host.d.ts
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
import { AppletAction, AppletMessage, ActionParams, AppletManifest, AppletDataEvent, AppletResizeEvent, AppletActionsEvent, AppletMessageRelay } from './shared';
|
2
|
-
interface LoadOpts {
|
3
|
-
unsafe?: boolean;
|
4
|
-
}
|
5
|
-
declare function load(url: string, container?: HTMLIFrameElement, opts?: LoadOpts): Promise<Applet>;
|
6
|
-
interface AppletOptions {
|
7
|
-
manifest: AppletManifest;
|
8
|
-
container: HTMLIFrameElement;
|
9
|
-
}
|
10
|
-
declare class Applet<T = any> extends EventTarget {
|
11
|
-
#private;
|
12
|
-
messageRelay: AppletMessageRelay;
|
13
|
-
url: string;
|
14
|
-
actions: AppletAction[];
|
15
|
-
container: HTMLIFrameElement;
|
16
|
-
type: string;
|
17
|
-
constructor(options: AppletOptions);
|
18
|
-
initializeListeners(): void;
|
19
|
-
get data(): T;
|
20
|
-
set data(data: T);
|
21
|
-
get manifest(): AppletManifest;
|
22
|
-
onresize(event: AppletResizeEvent): void;
|
23
|
-
onactions(event: AppletActionsEvent): void;
|
24
|
-
ondata(event: AppletDataEvent): void;
|
25
|
-
disconnect(): void;
|
26
|
-
dispatchAction(actionId: string, params: ActionParams): Promise<AppletMessage>;
|
27
|
-
}
|
28
|
-
export declare const applets: {
|
29
|
-
load: typeof load;
|
30
|
-
};
|
31
|
-
export { Applet };
|
package/dist/core/host.js
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
2
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
3
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
4
|
-
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");
|
5
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
6
|
-
};
|
7
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
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
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
-
};
|
12
|
-
var _Applet_manifest, _Applet_data;
|
13
|
-
import { AppletMessage, AppletDataMessage, AppletInitMessage, AppletDataEvent, AppletResizeEvent, AppletActionsEvent, AppletMessageRelay, } from './shared';
|
14
|
-
import { parseUrl } from '../lib/utils';
|
15
|
-
// Container for initializing applets without an explicit container
|
16
|
-
const hiddenContainer = document.createElement('iframe');
|
17
|
-
hiddenContainer.style.display = 'none';
|
18
|
-
document.body.appendChild(hiddenContainer);
|
19
|
-
const defaultOpts = {
|
20
|
-
unsafe: false,
|
21
|
-
};
|
22
|
-
// Load an applet object from a URL
|
23
|
-
async function load(url, container, opts) {
|
24
|
-
const _opts = Object.assign(defaultOpts, opts ?? {});
|
25
|
-
if (!container)
|
26
|
-
container = hiddenContainer;
|
27
|
-
url = parseUrl(url);
|
28
|
-
const manifest = await loadManifest(`${url}`);
|
29
|
-
// If unsafe enabled, allow same origin sandbox
|
30
|
-
// This is required for e.g. YouTube embeds
|
31
|
-
if (_opts.unsafe && manifest.unsafe) {
|
32
|
-
container.setAttribute('sandbox', 'allow-scripts allow-forms allow-same-origin');
|
33
|
-
}
|
34
|
-
else {
|
35
|
-
container.setAttribute('sandbox', 'allow-scripts allow-forms');
|
36
|
-
}
|
37
|
-
// Load the applet
|
38
|
-
const applet = new Applet({
|
39
|
-
manifest,
|
40
|
-
container,
|
41
|
-
});
|
42
|
-
return new Promise((resolve) => {
|
43
|
-
applet.messageRelay.on('ready', () => {
|
44
|
-
resolve(applet);
|
45
|
-
});
|
46
|
-
});
|
47
|
-
}
|
48
|
-
class Applet extends EventTarget {
|
49
|
-
constructor(options) {
|
50
|
-
super();
|
51
|
-
this.actions = [];
|
52
|
-
_Applet_manifest.set(this, void 0);
|
53
|
-
this.type = 'host';
|
54
|
-
_Applet_data.set(this, void 0);
|
55
|
-
this.container = options.container;
|
56
|
-
this.container.src = options.manifest.start_url;
|
57
|
-
this.messageRelay = new AppletMessageRelay(this.container.contentWindow);
|
58
|
-
__classPrivateFieldSet(this, _Applet_manifest, options.manifest, "f");
|
59
|
-
this.initializeListeners();
|
60
|
-
this.messageRelay.on('ready', () => {
|
61
|
-
this.messageRelay.send(new AppletInitMessage({ manifest: options.manifest }));
|
62
|
-
});
|
63
|
-
}
|
64
|
-
initializeListeners() {
|
65
|
-
this.messageRelay.on('data', (message) => {
|
66
|
-
__classPrivateFieldSet(this, _Applet_data, message.data, "f");
|
67
|
-
const dataEvent = new AppletDataEvent({ data: message.data });
|
68
|
-
if (typeof this.ondata === 'function')
|
69
|
-
this.ondata(dataEvent);
|
70
|
-
this.dispatchEvent(dataEvent);
|
71
|
-
});
|
72
|
-
this.messageRelay.on('resize', (message) => {
|
73
|
-
const resizeEvent = new AppletResizeEvent({
|
74
|
-
dimensions: message.dimensions,
|
75
|
-
});
|
76
|
-
if (typeof this.onresize === 'function')
|
77
|
-
this.onresize(resizeEvent);
|
78
|
-
this.dispatchEvent(resizeEvent);
|
79
|
-
});
|
80
|
-
this.messageRelay.on('actions', (message) => {
|
81
|
-
this.actions = message.actions;
|
82
|
-
const actionsEvent = new AppletActionsEvent({ actions: message.actions });
|
83
|
-
if (typeof this.onactions === 'function')
|
84
|
-
this.onactions(actionsEvent);
|
85
|
-
this.dispatchEvent(actionsEvent);
|
86
|
-
});
|
87
|
-
}
|
88
|
-
get data() {
|
89
|
-
return __classPrivateFieldGet(this, _Applet_data, "f");
|
90
|
-
}
|
91
|
-
set data(data) {
|
92
|
-
__classPrivateFieldSet(this, _Applet_data, data, "f");
|
93
|
-
this.messageRelay.send(new AppletDataMessage({ data }));
|
94
|
-
}
|
95
|
-
get manifest() {
|
96
|
-
return __classPrivateFieldGet(this, _Applet_manifest, "f");
|
97
|
-
}
|
98
|
-
onresize(event) { }
|
99
|
-
onactions(event) { }
|
100
|
-
ondata(event) { }
|
101
|
-
disconnect() {
|
102
|
-
this.container.src = 'about:blank';
|
103
|
-
}
|
104
|
-
async dispatchAction(actionId, params) {
|
105
|
-
const actionMessage = new AppletMessage('action', {
|
106
|
-
actionId,
|
107
|
-
params,
|
108
|
-
});
|
109
|
-
return await this.messageRelay.send(actionMessage);
|
110
|
-
}
|
111
|
-
}
|
112
|
-
_Applet_manifest = new WeakMap(), _Applet_data = new WeakMap();
|
113
|
-
// Loads a manifest and parses the JSON
|
114
|
-
async function loadManifest(baseUrl) {
|
115
|
-
baseUrl = parseUrl(baseUrl);
|
116
|
-
let manifest;
|
117
|
-
try {
|
118
|
-
const request = await fetch(`${baseUrl}/manifest.json`);
|
119
|
-
manifest = await request.json();
|
120
|
-
// TODO: Add verification this is a valid manifest
|
121
|
-
}
|
122
|
-
catch (e) {
|
123
|
-
console.error(e.message);
|
124
|
-
}
|
125
|
-
manifest.start_url = manifest.start_url
|
126
|
-
? parseUrl(manifest.start_url, baseUrl)
|
127
|
-
: baseUrl;
|
128
|
-
return manifest;
|
129
|
-
}
|
130
|
-
// Exports
|
131
|
-
export const applets = {
|
132
|
-
load,
|
133
|
-
};
|
134
|
-
export { Applet };
|
package/dist/lib/utils.d.ts
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
import { AppletAction } from '../core/shared';
|
2
|
-
export declare function parseUrl(url: string, base?: string): string;
|
3
|
-
export declare function createOpenAISchemaForAction(action: AppletAction): {
|
4
|
-
strict: boolean;
|
5
|
-
name: string;
|
6
|
-
schema: {
|
7
|
-
type: string;
|
8
|
-
required: string[];
|
9
|
-
properties: {
|
10
|
-
id: {
|
11
|
-
type: string;
|
12
|
-
};
|
13
|
-
params: import("../core/shared").JSONSchemaProperties;
|
14
|
-
};
|
15
|
-
additionalProperties: boolean;
|
16
|
-
};
|
17
|
-
};
|
package/dist/lib/utils.js
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
// Adds http/https to URLs, and prepends with window location if relative
|
2
|
-
export function parseUrl(url, base) {
|
3
|
-
if (['http', 'https'].includes(url.split('://')[0])) {
|
4
|
-
return url;
|
5
|
-
}
|
6
|
-
let path = trimSlashes(url);
|
7
|
-
url = `${base || window.location.origin}/${path}`;
|
8
|
-
return url;
|
9
|
-
}
|
10
|
-
function trimSlashes(str) {
|
11
|
-
return str.replace(/^\/+|\/+$/g, '');
|
12
|
-
}
|
13
|
-
export function createOpenAISchemaForAction(action) {
|
14
|
-
return {
|
15
|
-
strict: true,
|
16
|
-
name: 'action_schema',
|
17
|
-
schema: {
|
18
|
-
type: 'object',
|
19
|
-
required: Object.keys(action),
|
20
|
-
properties: {
|
21
|
-
id: { type: 'string' },
|
22
|
-
params: action.params,
|
23
|
-
},
|
24
|
-
additionalProperties: false,
|
25
|
-
},
|
26
|
-
};
|
27
|
-
}
|
28
|
-
// export async function loadAppletManifest(url: string): Promise<AppletManifest> {
|
29
|
-
// url = parseUrl(url);
|
30
|
-
// const request = await fetch(`${url}/manifest.json`);
|
31
|
-
// const appletManifest = await request.json();
|
32
|
-
// if (appletManifest.type !== 'applet') {
|
33
|
-
// throw new Error("URL doesn't point to a valid applet manifest.");
|
34
|
-
// }
|
35
|
-
// appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
36
|
-
// return appletManifest;
|
37
|
-
// }
|