@banastechnologie/antcloud-sdk 2.0.0
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 +213 -0
- package/ant-module.js +997 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BanasTechnologie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# 🐜 AntCloud SDK
|
|
2
|
+
|
|
3
|
+
**Robust WebAssembly module loading with caching and dynamic UI binding.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@banastechnologie/antcloud-sdk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<!-- Include SDK from CDN -->
|
|
12
|
+
<script type="module" src="https://cdn.jsdelivr.net/gh/yannbanas/antcloud-sdk@main/ant-module.js"></script>
|
|
13
|
+
|
|
14
|
+
<!-- Use the component -->
|
|
15
|
+
<ant-module name="fibonacci"></ant-module>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**That's it!** The SDK automatically:
|
|
19
|
+
- Fetches package info from registry
|
|
20
|
+
- Caches WASM in IndexedDB (downloads only on new version)
|
|
21
|
+
- Loads UI template
|
|
22
|
+
- Binds all events
|
|
23
|
+
- Executes WASM functions
|
|
24
|
+
|
|
25
|
+
## CDN Links
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!-- jsDelivr (recommended) -->
|
|
29
|
+
<script type="module" src="https://cdn.jsdelivr.net/gh/yannbanas/antcloud-sdk@main/ant-module.js"></script>
|
|
30
|
+
|
|
31
|
+
<!-- With specific version tag -->
|
|
32
|
+
<script type="module" src="https://cdn.jsdelivr.net/gh/yannbanas/antcloud-sdk@v2.0.0/ant-module.js"></script>
|
|
33
|
+
|
|
34
|
+
<!-- unpkg (after npm publish) -->
|
|
35
|
+
<script type="module" src="https://unpkg.com/@banastechnologie/antcloud-sdk"></script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
| Feature | Description |
|
|
41
|
+
|---------|-------------|
|
|
42
|
+
| 📦 **IndexedDB Cache** | WASM downloaded only once per version |
|
|
43
|
+
| ⚡ **Fast Reload** | Cached modules load instantly |
|
|
44
|
+
| 🎨 **Dynamic UI** | Binds to `data-ant-*` attributes automatically |
|
|
45
|
+
| 🔄 **Version Check** | Always uses latest version from registry |
|
|
46
|
+
| 🔌 **Clean API** | Simple programmatic control |
|
|
47
|
+
|
|
48
|
+
## How Caching Works
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
First visit:
|
|
52
|
+
└─► Fetch package info (check version)
|
|
53
|
+
└─► No cache → Download WASM → Store in IndexedDB
|
|
54
|
+
└─► Load UI → Render → Ready!
|
|
55
|
+
|
|
56
|
+
Return visit (same version):
|
|
57
|
+
└─► Fetch package info → v1.2.0
|
|
58
|
+
└─► Cache exists with v1.2.0 → Use cached WASM ✅
|
|
59
|
+
└─► No download needed!
|
|
60
|
+
|
|
61
|
+
After new version published:
|
|
62
|
+
└─► Fetch package info → v1.3.0 (new!)
|
|
63
|
+
└─► Cache has v1.2.0 → Download v1.3.0
|
|
64
|
+
└─► Update cache → Ready!
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Attributes
|
|
68
|
+
|
|
69
|
+
| Attribute | Description | Default |
|
|
70
|
+
|-----------|-------------|---------|
|
|
71
|
+
| `name` | Package name (required) | - |
|
|
72
|
+
| `registry` | Registry URL | `https://antcloud-registry.up.railway.app` |
|
|
73
|
+
| `no-cache` | Disable caching (for development) | - |
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<!-- Basic usage -->
|
|
77
|
+
<ant-module name="fibonacci"></ant-module>
|
|
78
|
+
|
|
79
|
+
<!-- Custom registry -->
|
|
80
|
+
<ant-module name="my-package" registry="https://my-registry.com"></ant-module>
|
|
81
|
+
|
|
82
|
+
<!-- Disable cache (dev mode) -->
|
|
83
|
+
<ant-module name="fibonacci" no-cache></ant-module>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## UI Template Attributes
|
|
87
|
+
|
|
88
|
+
The SDK binds these `data-ant-*` attributes in your UI template:
|
|
89
|
+
|
|
90
|
+
| Attribute | Description |
|
|
91
|
+
|-----------|-------------|
|
|
92
|
+
| `data-ant-function="name"` | Function selector (tab/button) |
|
|
93
|
+
| `data-ant-inputs="name"` | Input group for a function |
|
|
94
|
+
| `data-ant-input="n"` | Input field |
|
|
95
|
+
| `data-ant-type="i32\|i64\|f32\|f64"` | Input type for conversion |
|
|
96
|
+
| `data-ant-display="n"` | Display value (for sliders) |
|
|
97
|
+
| `data-ant-execute` | Execute button |
|
|
98
|
+
| `data-ant-output` | Result container |
|
|
99
|
+
| `data-ant-value` | Result value |
|
|
100
|
+
| `data-ant-time` | Execution time |
|
|
101
|
+
| `data-ant-expr` | Expression called |
|
|
102
|
+
| `data-ant-status-dot` | Status indicator |
|
|
103
|
+
| `data-ant-status-text` | Status text |
|
|
104
|
+
| `data-ant-true-label` | Label for true (bool functions) |
|
|
105
|
+
| `data-ant-false-label` | Label for false (bool functions) |
|
|
106
|
+
|
|
107
|
+
## Template Variables
|
|
108
|
+
|
|
109
|
+
Use these in your UI HTML:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
{{name}} → Package name
|
|
113
|
+
{{version}} → Latest version
|
|
114
|
+
{{author}} → Package author
|
|
115
|
+
{{description}} → Package description
|
|
116
|
+
{{license}} → Package license
|
|
117
|
+
{{downloads}} → Download count
|
|
118
|
+
{{stars}} → Star count
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## JavaScript API
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const module = document.querySelector('ant-module');
|
|
125
|
+
|
|
126
|
+
// Wait for ready
|
|
127
|
+
module.addEventListener('ant-ready', (e) => {
|
|
128
|
+
console.log(e.detail.name, e.detail.version, e.detail.functions);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Listen for results
|
|
132
|
+
module.addEventListener('ant-result', (e) => {
|
|
133
|
+
console.log(e.detail.function, e.detail.result, e.detail.time);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Listen for errors
|
|
137
|
+
module.addEventListener('ant-error', (e) => {
|
|
138
|
+
console.error(e.detail.error);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Programmatic control
|
|
142
|
+
module.setValues({ n: 42 });
|
|
143
|
+
module.getValues(); // { n: "42" }
|
|
144
|
+
module.setFunction('is_fibonacci');
|
|
145
|
+
module.getFunction(); // "is_fibonacci"
|
|
146
|
+
module.getFunctions(); // ["fibonacci", "is_fibonacci", ...]
|
|
147
|
+
|
|
148
|
+
// Execute and get result
|
|
149
|
+
const result = await module.invoke('fibonacci', { n: 50 });
|
|
150
|
+
|
|
151
|
+
// Clear cache (force re-download)
|
|
152
|
+
await module.clearCache();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Publishing Packages
|
|
156
|
+
|
|
157
|
+
Create a WASM module with UI and publish to AntHive registry:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Publish package with UI
|
|
161
|
+
curl -X POST https://antcloud-registry.up.railway.app/api/v1/packages \
|
|
162
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
163
|
+
-F "manifest=@anthive.yaml" \
|
|
164
|
+
-F "wasm=@my-function.wasm" \
|
|
165
|
+
-F "ui=@ui.html"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Example UI Template
|
|
169
|
+
|
|
170
|
+
Your `ui.html` should be **HTML/CSS only** (no JavaScript). The SDK handles all logic:
|
|
171
|
+
|
|
172
|
+
```html
|
|
173
|
+
<style>
|
|
174
|
+
:host { display: block; font-family: system-ui; }
|
|
175
|
+
/* ... your styles ... */
|
|
176
|
+
</style>
|
|
177
|
+
|
|
178
|
+
<div class="card">
|
|
179
|
+
<h2>{{name}}</h2>
|
|
180
|
+
<p>{{description}}</p>
|
|
181
|
+
|
|
182
|
+
<!-- Function tabs -->
|
|
183
|
+
<div class="tabs">
|
|
184
|
+
<button data-ant-function="calculate">calculate()</button>
|
|
185
|
+
<button data-ant-function="check">check()</button>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<!-- Inputs -->
|
|
189
|
+
<div data-ant-inputs="calculate">
|
|
190
|
+
<input type="range" data-ant-input="n" data-ant-type="i32" min="0" max="100" value="10">
|
|
191
|
+
<span data-ant-display="n">10</span>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Execute -->
|
|
195
|
+
<button data-ant-execute>▶ Execute</button>
|
|
196
|
+
|
|
197
|
+
<!-- Result -->
|
|
198
|
+
<div data-ant-output>
|
|
199
|
+
<span data-ant-value></span>
|
|
200
|
+
<span data-ant-time></span>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Registry
|
|
206
|
+
|
|
207
|
+
Default registry: [antcloud-registry.up.railway.app](https://antcloud-registry.up.railway.app)
|
|
208
|
+
|
|
209
|
+
Browse available packages and documentation at the registry homepage.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT © [BanasTechnologie](https://github.com/yannbanas)
|
package/ant-module.js
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🐜 AntCloud SDK v2.0
|
|
3
|
+
*
|
|
4
|
+
* Robust WebComponent for WASM modules with:
|
|
5
|
+
* - IndexedDB caching (downloads only on new version)
|
|
6
|
+
* - Dynamic UI binding via data-ant-* attributes
|
|
7
|
+
* - Full manifest/exports support
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Registry = source of truth (manifest, WASM, UI)
|
|
11
|
+
* - SDK = engine (cache, binding, execution)
|
|
12
|
+
* - UI = presentation only (HTML/CSS + data-ant-*)
|
|
13
|
+
*
|
|
14
|
+
* @author BanasTechnologie
|
|
15
|
+
* @license MIT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REGISTRY = 'https://antcloud-registry.up.railway.app';
|
|
19
|
+
const CACHE_DB_NAME = 'antcloud-cache';
|
|
20
|
+
const CACHE_DB_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
class AntModule extends HTMLElement {
|
|
23
|
+
static get observedAttributes() {
|
|
24
|
+
return ['name', 'registry', 'runtime', 'no-cache'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.attachShadow({ mode: 'open' });
|
|
30
|
+
|
|
31
|
+
this._packageInfo = null;
|
|
32
|
+
this._wasmModule = null; // Compiled WebAssembly.Module
|
|
33
|
+
this._wasmInstance = null; // WebAssembly.Instance
|
|
34
|
+
this._selectedFunction = null;
|
|
35
|
+
this._values = {};
|
|
36
|
+
this._state = 'loading';
|
|
37
|
+
this._db = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
this._showLoading();
|
|
42
|
+
this._init();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get name() { return this.getAttribute('name'); }
|
|
46
|
+
get registry() { return this.getAttribute('registry') || DEFAULT_REGISTRY; }
|
|
47
|
+
get noCache() { return this.hasAttribute('no-cache'); }
|
|
48
|
+
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
// 🗄️ INDEXEDDB CACHE
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
async _openDB() {
|
|
54
|
+
if (this._db) return this._db;
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const request = indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION);
|
|
58
|
+
|
|
59
|
+
request.onerror = () => reject(request.error);
|
|
60
|
+
|
|
61
|
+
request.onsuccess = () => {
|
|
62
|
+
this._db = request.result;
|
|
63
|
+
resolve(this._db);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
request.onupgradeneeded = (event) => {
|
|
67
|
+
const db = event.target.result;
|
|
68
|
+
|
|
69
|
+
// Store for WASM modules
|
|
70
|
+
if (!db.objectStoreNames.contains('modules')) {
|
|
71
|
+
const store = db.createObjectStore('modules', { keyPath: 'name' });
|
|
72
|
+
store.createIndex('version', 'version', { unique: false });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Store for UI templates
|
|
76
|
+
if (!db.objectStoreNames.contains('ui')) {
|
|
77
|
+
db.createObjectStore('ui', { keyPath: 'name' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Store for package info
|
|
81
|
+
if (!db.objectStoreNames.contains('packages')) {
|
|
82
|
+
db.createObjectStore('packages', { keyPath: 'name' });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async _getCached(storeName, key) {
|
|
89
|
+
try {
|
|
90
|
+
const db = await this._openDB();
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
93
|
+
const store = tx.objectStore(storeName);
|
|
94
|
+
const request = store.get(key);
|
|
95
|
+
request.onsuccess = () => resolve(request.result);
|
|
96
|
+
request.onerror = () => reject(request.error);
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.warn('🐜 Cache read failed:', e);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async _setCache(storeName, data) {
|
|
105
|
+
try {
|
|
106
|
+
const db = await this._openDB();
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
109
|
+
const store = tx.objectStore(storeName);
|
|
110
|
+
const request = store.put(data);
|
|
111
|
+
request.onsuccess = () => resolve();
|
|
112
|
+
request.onerror = () => reject(request.error);
|
|
113
|
+
});
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.warn('🐜 Cache write failed:', e);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
|
+
// 📦 LOADING WITH CACHE
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
|
|
123
|
+
async _init() {
|
|
124
|
+
try {
|
|
125
|
+
console.log(`🐜 Loading module: ${this.name}`);
|
|
126
|
+
|
|
127
|
+
// 1. Get package info (always fetch to check version)
|
|
128
|
+
this._packageInfo = await this._fetchPackageInfo();
|
|
129
|
+
const latestVersion = this._packageInfo.latest_version;
|
|
130
|
+
console.log(`🐜 Registry version: ${latestVersion}`);
|
|
131
|
+
|
|
132
|
+
// 2. Check cache for WASM
|
|
133
|
+
const cached = await this._getCached('modules', this.name);
|
|
134
|
+
|
|
135
|
+
if (!this.noCache && cached && cached.version === latestVersion && cached.wasmBytes) {
|
|
136
|
+
// Use cached WASM
|
|
137
|
+
console.log(`🐜 Using cached WASM v${cached.version}`);
|
|
138
|
+
await this._instantiateWasm(cached.wasmBytes);
|
|
139
|
+
} else {
|
|
140
|
+
// Download new version
|
|
141
|
+
if (this.noCache) console.log(`🐜 Cache disabled, downloading fresh...`);
|
|
142
|
+
console.log(`🐜 Downloading WASM v${latestVersion}...`);
|
|
143
|
+
const wasmBytes = await this._downloadWasm(latestVersion);
|
|
144
|
+
await this._instantiateWasm(wasmBytes);
|
|
145
|
+
|
|
146
|
+
// Cache it (unless no-cache)
|
|
147
|
+
if (!this.noCache) {
|
|
148
|
+
await this._setCache('modules', {
|
|
149
|
+
name: this.name,
|
|
150
|
+
version: latestVersion,
|
|
151
|
+
wasmBytes: wasmBytes,
|
|
152
|
+
cachedAt: Date.now()
|
|
153
|
+
});
|
|
154
|
+
console.log(`🐜 WASM cached`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 3. Load UI (with cache)
|
|
159
|
+
const uiHtml = await this._loadUI(latestVersion);
|
|
160
|
+
|
|
161
|
+
// 4. Render UI
|
|
162
|
+
this._renderUI(uiHtml);
|
|
163
|
+
|
|
164
|
+
// 5. Init and bind
|
|
165
|
+
this._initFromManifest();
|
|
166
|
+
this._bindEvents();
|
|
167
|
+
|
|
168
|
+
this._state = 'ready';
|
|
169
|
+
this._updateStatus('ready');
|
|
170
|
+
|
|
171
|
+
console.log(`🐜 Ready! Functions: ${this.getFunctions().join(', ')}`);
|
|
172
|
+
|
|
173
|
+
this.dispatchEvent(new CustomEvent('ant-ready', {
|
|
174
|
+
detail: {
|
|
175
|
+
name: this.name,
|
|
176
|
+
version: latestVersion,
|
|
177
|
+
functions: this.getFunctions()
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('🐜 Error:', err);
|
|
183
|
+
this._showError(err.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async _fetchPackageInfo() {
|
|
188
|
+
const url = `${this.registry}/api/v1/packages/${this.name}`;
|
|
189
|
+
const res = await fetch(url);
|
|
190
|
+
if (!res.ok) throw new Error(`Package not found: ${this.name}`);
|
|
191
|
+
return res.json();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async _downloadWasm(version) {
|
|
195
|
+
const url = `${this.registry}/api/v1/packages/${this.name}/${version}/download`;
|
|
196
|
+
const res = await fetch(url);
|
|
197
|
+
if (!res.ok) throw new Error(`WASM download failed (${res.status})`);
|
|
198
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _instantiateWasm(bytes) {
|
|
202
|
+
this._wasmModule = await WebAssembly.compile(bytes);
|
|
203
|
+
this._wasmInstance = await WebAssembly.instantiate(this._wasmModule, {
|
|
204
|
+
env: { abort: () => console.warn('WASM abort called') }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async _loadUI(version) {
|
|
209
|
+
// Check cache - but also check if it's not too old (max 1 hour for UI)
|
|
210
|
+
const cached = await this._getCached('ui', this.name);
|
|
211
|
+
const cacheMaxAge = 60 * 60 * 1000; // 1 hour
|
|
212
|
+
const isCacheValid = !this.noCache
|
|
213
|
+
&& cached
|
|
214
|
+
&& cached.version === version
|
|
215
|
+
&& cached.cachedAt
|
|
216
|
+
&& (Date.now() - cached.cachedAt) < cacheMaxAge;
|
|
217
|
+
|
|
218
|
+
if (isCacheValid) {
|
|
219
|
+
console.log(`🐜 Using cached UI`);
|
|
220
|
+
return cached.html;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fetch from registry
|
|
224
|
+
let html = null;
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`${this.registry}/api/v1/packages/${this.name}/${version}/ui`);
|
|
227
|
+
if (res.ok) {
|
|
228
|
+
html = await res.text();
|
|
229
|
+
console.log(`🐜 UI loaded from registry`);
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {}
|
|
232
|
+
|
|
233
|
+
// Fallback to latest
|
|
234
|
+
if (!html) {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch(`${this.registry}/api/v1/packages/${this.name}/ui`);
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
html = await res.text();
|
|
239
|
+
console.log(`🐜 UI loaded from registry (latest)`);
|
|
240
|
+
}
|
|
241
|
+
} catch (e) {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback local
|
|
245
|
+
if (!html) {
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch(`./modules/${this.name}/ui.html`);
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
html = await res.text();
|
|
250
|
+
console.log(`🐜 UI loaded from local`);
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Generate default if nothing found
|
|
256
|
+
if (!html) {
|
|
257
|
+
console.log(`🐜 Generating default UI`);
|
|
258
|
+
html = this._generateDefaultUI();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Cache UI (unless no-cache)
|
|
262
|
+
if (!this.noCache) {
|
|
263
|
+
await this._setCache('ui', {
|
|
264
|
+
name: this.name,
|
|
265
|
+
version: version,
|
|
266
|
+
html: html,
|
|
267
|
+
cachedAt: Date.now()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return html;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
|
+
// 🎨 RENDERING
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
277
|
+
|
|
278
|
+
_renderUI(html) {
|
|
279
|
+
const pkg = this._packageInfo;
|
|
280
|
+
|
|
281
|
+
// Replace template variables
|
|
282
|
+
let rendered = html
|
|
283
|
+
.replace(/\{\{name\}\}/g, pkg?.name || this.name)
|
|
284
|
+
.replace(/\{\{version\}\}/g, pkg?.latest_version || '1.0.0')
|
|
285
|
+
.replace(/\{\{author\}\}/g, pkg?.author || 'unknown')
|
|
286
|
+
.replace(/\{\{description\}\}/g, pkg?.description || '')
|
|
287
|
+
.replace(/\{\{license\}\}/g, pkg?.license || 'MIT')
|
|
288
|
+
.replace(/\{\{downloads\}\}/g, pkg?.downloads || 0)
|
|
289
|
+
.replace(/\{\{stars\}\}/g, pkg?.stars || 0);
|
|
290
|
+
|
|
291
|
+
// Remove any <script> tags - SDK handles all logic
|
|
292
|
+
rendered = rendered.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
293
|
+
|
|
294
|
+
this.shadowRoot.innerHTML = rendered;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_showLoading() {
|
|
298
|
+
this.shadowRoot.innerHTML = `
|
|
299
|
+
<style>
|
|
300
|
+
:host { display: block; font-family: system-ui, sans-serif; }
|
|
301
|
+
.loading { padding: 40px; text-align: center; background: #111; border-radius: 12px; border: 1px solid rgba(245,158,11,0.2); }
|
|
302
|
+
.loading .icon { font-size: 48px; margin-bottom: 16px; animation: pulse 1.5s ease infinite; }
|
|
303
|
+
.loading .text { color: #888; }
|
|
304
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
305
|
+
</style>
|
|
306
|
+
<div class="loading">
|
|
307
|
+
<div class="icon">🐜</div>
|
|
308
|
+
<div class="text">Loading ${this.name}...</div>
|
|
309
|
+
</div>
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_showError(message) {
|
|
314
|
+
this.shadowRoot.innerHTML = `
|
|
315
|
+
<style>
|
|
316
|
+
:host { display: block; font-family: system-ui, sans-serif; }
|
|
317
|
+
.error { padding: 40px; text-align: center; background: #111; border-radius: 12px; border: 1px solid rgba(239,68,68,0.3); }
|
|
318
|
+
.error .icon { font-size: 48px; margin-bottom: 16px; }
|
|
319
|
+
.error .msg { color: #ef4444; margin-bottom: 16px; }
|
|
320
|
+
.error button { padding: 10px 20px; background: transparent; border: 1px solid rgba(245,158,11,0.3); border-radius: 8px; color: #e5e5e5; cursor: pointer; }
|
|
321
|
+
.error button:hover { background: rgba(245,158,11,0.1); }
|
|
322
|
+
</style>
|
|
323
|
+
<div class="error">
|
|
324
|
+
<div class="icon">⚠️</div>
|
|
325
|
+
<div class="msg">${message}</div>
|
|
326
|
+
<button onclick="this.getRootNode().host._init()">Retry</button>
|
|
327
|
+
</div>
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_generateDefaultUI() {
|
|
332
|
+
const pkg = this._packageInfo;
|
|
333
|
+
const funcs = this.getFunctions();
|
|
334
|
+
const version = pkg?.versions?.find(v => v.version === pkg.latest_version);
|
|
335
|
+
const exports = version?.exports || [];
|
|
336
|
+
|
|
337
|
+
// Generate function tabs
|
|
338
|
+
const tabs = funcs.map((f, i) => {
|
|
339
|
+
const exp = exports.find(e => e.name === f);
|
|
340
|
+
const label = exp?.signature ? `${f}${exp.signature.split('->')[0].trim()}` : `${f}()`;
|
|
341
|
+
return `<button class="tab ${i === 0 ? 'active' : ''}" data-ant-function="${f}">${label}</button>`;
|
|
342
|
+
}).join('');
|
|
343
|
+
|
|
344
|
+
// Generate input groups for each function
|
|
345
|
+
const inputGroups = funcs.map((f, i) => {
|
|
346
|
+
const exp = exports.find(e => e.name === f);
|
|
347
|
+
const params = this._parseSignature(exp?.signature);
|
|
348
|
+
|
|
349
|
+
const inputs = params.map(p => `
|
|
350
|
+
<div class="field">
|
|
351
|
+
<label class="label">${p.name} <span class="type">${p.type}</span></label>
|
|
352
|
+
${p.type === 'i64' || p.type === 'u64'
|
|
353
|
+
? `<input type="number" class="input" data-ant-input="${p.name}" data-ant-type="${p.type}" value="10">`
|
|
354
|
+
: `<div class="slider-row">
|
|
355
|
+
<input type="range" class="slider" data-ant-input="${p.name}" data-ant-type="${p.type}" min="0" max="92" value="10">
|
|
356
|
+
<span class="value-display" data-ant-display="${p.name}">10</span>
|
|
357
|
+
</div>`
|
|
358
|
+
}
|
|
359
|
+
</div>
|
|
360
|
+
`).join('');
|
|
361
|
+
|
|
362
|
+
return `<div class="input-group ${i === 0 ? '' : 'hidden'}" data-ant-inputs="${f}">${inputs || '<p class="no-params">No parameters</p>'}</div>`;
|
|
363
|
+
}).join('');
|
|
364
|
+
|
|
365
|
+
return `
|
|
366
|
+
<style>
|
|
367
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
368
|
+
|
|
369
|
+
:host {
|
|
370
|
+
--primary: #F59E0B;
|
|
371
|
+
--primary-dark: #D97706;
|
|
372
|
+
--bg: #0a0a0a;
|
|
373
|
+
--surface: #111;
|
|
374
|
+
--text: #e5e5e5;
|
|
375
|
+
--text-muted: #888;
|
|
376
|
+
--success: #22c55e;
|
|
377
|
+
--error: #ef4444;
|
|
378
|
+
display: block;
|
|
379
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.card {
|
|
383
|
+
background: linear-gradient(145deg, var(--surface), var(--bg));
|
|
384
|
+
border: 1px solid rgba(245,158,11,0.15);
|
|
385
|
+
border-radius: 16px;
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.card::before {
|
|
390
|
+
content: '';
|
|
391
|
+
display: block;
|
|
392
|
+
height: 2px;
|
|
393
|
+
background: linear-gradient(90deg, transparent, var(--primary), transparent);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.header {
|
|
397
|
+
padding: 20px;
|
|
398
|
+
background: rgba(0,0,0,0.3);
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
gap: 16px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.logo {
|
|
405
|
+
width: 52px;
|
|
406
|
+
height: 52px;
|
|
407
|
+
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
|
408
|
+
border-radius: 12px;
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
justify-content: center;
|
|
412
|
+
font-size: 26px;
|
|
413
|
+
box-shadow: 0 4px 16px rgba(245,158,11,0.25);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.info { flex: 1; }
|
|
417
|
+
.title { font-size: 18px; font-weight: 600; color: var(--text); margin: 0 0 4px; }
|
|
418
|
+
.desc { font-size: 13px; color: var(--text-muted); margin: 0; }
|
|
419
|
+
|
|
420
|
+
.badges { display: flex; gap: 8px; margin-top: 8px; }
|
|
421
|
+
.badge {
|
|
422
|
+
padding: 4px 10px;
|
|
423
|
+
background: rgba(245,158,11,0.1);
|
|
424
|
+
border-radius: 6px;
|
|
425
|
+
font-size: 11px;
|
|
426
|
+
font-family: 'JetBrains Mono', monospace;
|
|
427
|
+
color: var(--primary);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.status {
|
|
431
|
+
display: flex;
|
|
432
|
+
align-items: center;
|
|
433
|
+
gap: 8px;
|
|
434
|
+
font-size: 12px;
|
|
435
|
+
color: var(--text-muted);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.status-dot {
|
|
439
|
+
width: 10px;
|
|
440
|
+
height: 10px;
|
|
441
|
+
border-radius: 50%;
|
|
442
|
+
background: var(--text-muted);
|
|
443
|
+
}
|
|
444
|
+
.status-dot.ready { background: var(--success); box-shadow: 0 0 8px var(--success); }
|
|
445
|
+
.status-dot.executing { background: var(--primary); animation: pulse 0.6s ease infinite; }
|
|
446
|
+
|
|
447
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
448
|
+
|
|
449
|
+
.body { padding: 20px; }
|
|
450
|
+
|
|
451
|
+
.tabs {
|
|
452
|
+
display: flex;
|
|
453
|
+
gap: 8px;
|
|
454
|
+
margin-bottom: 20px;
|
|
455
|
+
flex-wrap: wrap;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.tab {
|
|
459
|
+
padding: 10px 14px;
|
|
460
|
+
background: rgba(255,255,255,0.03);
|
|
461
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
462
|
+
border-radius: 8px;
|
|
463
|
+
color: var(--text-muted);
|
|
464
|
+
font-size: 12px;
|
|
465
|
+
font-family: 'JetBrains Mono', monospace;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
transition: all 0.2s;
|
|
468
|
+
}
|
|
469
|
+
.tab:hover { border-color: rgba(245,158,11,0.3); color: var(--text); }
|
|
470
|
+
.tab.active { background: rgba(245,158,11,0.15); border-color: var(--primary); color: var(--primary); }
|
|
471
|
+
|
|
472
|
+
.input-group { margin-bottom: 16px; }
|
|
473
|
+
.input-group.hidden { display: none; }
|
|
474
|
+
|
|
475
|
+
.field { margin-bottom: 14px; }
|
|
476
|
+
|
|
477
|
+
.label {
|
|
478
|
+
display: flex;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
align-items: center;
|
|
481
|
+
font-size: 12px;
|
|
482
|
+
color: var(--text-muted);
|
|
483
|
+
text-transform: uppercase;
|
|
484
|
+
margin-bottom: 8px;
|
|
485
|
+
}
|
|
486
|
+
.type {
|
|
487
|
+
font-family: 'JetBrains Mono', monospace;
|
|
488
|
+
font-size: 10px;
|
|
489
|
+
padding: 2px 6px;
|
|
490
|
+
background: rgba(255,255,255,0.05);
|
|
491
|
+
border-radius: 4px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.slider-row {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
gap: 14px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.slider {
|
|
501
|
+
flex: 1;
|
|
502
|
+
-webkit-appearance: none;
|
|
503
|
+
height: 6px;
|
|
504
|
+
background: #222;
|
|
505
|
+
border-radius: 3px;
|
|
506
|
+
}
|
|
507
|
+
.slider::-webkit-slider-thumb {
|
|
508
|
+
-webkit-appearance: none;
|
|
509
|
+
width: 20px;
|
|
510
|
+
height: 20px;
|
|
511
|
+
background: linear-gradient(135deg, var(--primary), #FBBF24);
|
|
512
|
+
border-radius: 50%;
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
box-shadow: 0 4px 12px rgba(245,158,11,0.4);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.value-display {
|
|
518
|
+
min-width: 50px;
|
|
519
|
+
padding: 10px 14px;
|
|
520
|
+
background: var(--bg);
|
|
521
|
+
border: 1px solid rgba(245,158,11,0.15);
|
|
522
|
+
border-radius: 8px;
|
|
523
|
+
text-align: center;
|
|
524
|
+
font-family: 'JetBrains Mono', monospace;
|
|
525
|
+
font-size: 16px;
|
|
526
|
+
font-weight: 600;
|
|
527
|
+
color: var(--primary);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.input {
|
|
531
|
+
width: 100%;
|
|
532
|
+
padding: 12px 16px;
|
|
533
|
+
background: var(--bg);
|
|
534
|
+
border: 1px solid rgba(245,158,11,0.15);
|
|
535
|
+
border-radius: 10px;
|
|
536
|
+
color: var(--text);
|
|
537
|
+
font-size: 15px;
|
|
538
|
+
font-family: 'JetBrains Mono', monospace;
|
|
539
|
+
outline: none;
|
|
540
|
+
}
|
|
541
|
+
.input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(245,158,11,0.1); }
|
|
542
|
+
|
|
543
|
+
.no-params {
|
|
544
|
+
color: var(--text-muted);
|
|
545
|
+
font-size: 13px;
|
|
546
|
+
font-style: italic;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.execute {
|
|
550
|
+
width: 100%;
|
|
551
|
+
padding: 14px;
|
|
552
|
+
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
|
553
|
+
border: none;
|
|
554
|
+
border-radius: 10px;
|
|
555
|
+
color: #000;
|
|
556
|
+
font-weight: 600;
|
|
557
|
+
font-size: 15px;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
transition: all 0.2s;
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
justify-content: center;
|
|
563
|
+
gap: 8px;
|
|
564
|
+
}
|
|
565
|
+
.execute:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(245,158,11,0.3); }
|
|
566
|
+
.execute:disabled { opacity: 0.5; cursor: wait; transform: none; }
|
|
567
|
+
.execute.loading .btn-text { display: none; }
|
|
568
|
+
.execute .spinner { display: none; width: 18px; height: 18px; border: 2px solid rgba(0,0,0,0.2); border-top-color: #000; border-radius: 50%; animation: spin 0.6s linear infinite; }
|
|
569
|
+
.execute.loading .spinner { display: block; }
|
|
570
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
571
|
+
|
|
572
|
+
.result {
|
|
573
|
+
margin-top: 20px;
|
|
574
|
+
padding: 18px;
|
|
575
|
+
background: linear-gradient(135deg, rgba(34,197,94,0.08), rgba(34,197,94,0.02));
|
|
576
|
+
border: 1px solid rgba(34,197,94,0.25);
|
|
577
|
+
border-radius: 12px;
|
|
578
|
+
display: none;
|
|
579
|
+
}
|
|
580
|
+
.result.visible { display: block; animation: fadeIn 0.2s ease; }
|
|
581
|
+
.result.error { background: linear-gradient(135deg, rgba(239,68,68,0.08), rgba(239,68,68,0.02)); border-color: rgba(239,68,68,0.25); }
|
|
582
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
|
|
583
|
+
|
|
584
|
+
.result-header { display: flex; justify-content: space-between; margin-bottom: 10px; }
|
|
585
|
+
.result-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--success); }
|
|
586
|
+
.result.error .result-label { color: var(--error); }
|
|
587
|
+
.result-time { font-size: 11px; font-family: 'JetBrains Mono', monospace; color: #555; }
|
|
588
|
+
|
|
589
|
+
.result-value {
|
|
590
|
+
font-family: 'JetBrains Mono', monospace;
|
|
591
|
+
font-size: 28px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
color: var(--success);
|
|
594
|
+
word-break: break-all;
|
|
595
|
+
}
|
|
596
|
+
.result.error .result-value { color: var(--error); font-size: 14px; }
|
|
597
|
+
.result-value.bool-true { color: var(--success); font-size: 18px; }
|
|
598
|
+
.result-value.bool-false { color: var(--error); font-size: 18px; }
|
|
599
|
+
|
|
600
|
+
.result-expr { margin-top: 10px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: #444; }
|
|
601
|
+
|
|
602
|
+
.footer {
|
|
603
|
+
padding: 14px 20px;
|
|
604
|
+
background: rgba(0,0,0,0.2);
|
|
605
|
+
border-top: 1px solid rgba(245,158,11,0.1);
|
|
606
|
+
display: flex;
|
|
607
|
+
justify-content: space-between;
|
|
608
|
+
align-items: center;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
612
|
+
.tag { padding: 4px 10px; background: rgba(245,158,11,0.1); border-radius: 6px; font-size: 10px; color: var(--primary); }
|
|
613
|
+
|
|
614
|
+
.mode { font-size: 11px; color: #555; display: flex; align-items: center; gap: 6px; }
|
|
615
|
+
|
|
616
|
+
.hidden { display: none !important; }
|
|
617
|
+
</style>
|
|
618
|
+
|
|
619
|
+
<div class="card">
|
|
620
|
+
<div class="header">
|
|
621
|
+
<div class="logo">🔢</div>
|
|
622
|
+
<div class="info">
|
|
623
|
+
<h2 class="title">{{name}}</h2>
|
|
624
|
+
<p class="desc">{{description}}</p>
|
|
625
|
+
<div class="badges">
|
|
626
|
+
<span class="badge">v{{version}}</span>
|
|
627
|
+
<span class="badge">by {{author}}</span>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="status">
|
|
631
|
+
<span class="status-dot ready" data-ant-status-dot></span>
|
|
632
|
+
<span data-ant-status-text>Ready</span>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div class="body">
|
|
637
|
+
<div class="tabs">${tabs}</div>
|
|
638
|
+
${inputGroups}
|
|
639
|
+
|
|
640
|
+
<button class="execute" data-ant-execute>
|
|
641
|
+
<span class="btn-text">▶ Execute</span>
|
|
642
|
+
<span class="spinner"></span>
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
<div class="result" data-ant-output>
|
|
646
|
+
<div class="result-header">
|
|
647
|
+
<span class="result-label">Result</span>
|
|
648
|
+
<span class="result-time" data-ant-time></span>
|
|
649
|
+
</div>
|
|
650
|
+
<div class="result-value" data-ant-value data-ant-format="locale"></div>
|
|
651
|
+
<div class="result-expr" data-ant-expr></div>
|
|
652
|
+
<span class="hidden" data-ant-true-label>✅ Yes!</span>
|
|
653
|
+
<span class="hidden" data-ant-false-label>❌ No</span>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div class="footer">
|
|
658
|
+
<div class="tags">
|
|
659
|
+
${(pkg?.tags || ['wasm']).map(t => `<span class="tag">${t}</span>`).join('')}
|
|
660
|
+
</div>
|
|
661
|
+
<div class="mode">💻 Local WASM</div>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
_parseSignature(signature) {
|
|
668
|
+
if (!signature) return [{ name: 'n', type: 'i32' }];
|
|
669
|
+
|
|
670
|
+
// Parse "(i32, i64) -> i64" or "(n: i32) -> i64"
|
|
671
|
+
const match = signature.match(/\(([^)]*)\)/);
|
|
672
|
+
if (!match) return [{ name: 'n', type: 'i32' }];
|
|
673
|
+
|
|
674
|
+
const paramsStr = match[1].trim();
|
|
675
|
+
if (!paramsStr) return [];
|
|
676
|
+
|
|
677
|
+
return paramsStr.split(',').map((p, i) => {
|
|
678
|
+
p = p.trim();
|
|
679
|
+
if (p.includes(':')) {
|
|
680
|
+
const [name, type] = p.split(':').map(s => s.trim());
|
|
681
|
+
return { name, type };
|
|
682
|
+
}
|
|
683
|
+
return { name: ['n', 'x', 'y', 'z'][i] || `arg${i}`, type: p };
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
688
|
+
// 🔗 EVENT BINDING
|
|
689
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
690
|
+
|
|
691
|
+
_initFromManifest() {
|
|
692
|
+
const funcs = this.getFunctions();
|
|
693
|
+
|
|
694
|
+
// Select first function
|
|
695
|
+
if (funcs.length > 0) {
|
|
696
|
+
this._selectedFunction = funcs[0];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Init values from DOM
|
|
700
|
+
this._collectValues();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
_collectValues() {
|
|
704
|
+
this._values = {};
|
|
705
|
+
|
|
706
|
+
// Find visible input group
|
|
707
|
+
const visibleGroup = Array.from(this.shadowRoot.querySelectorAll('[data-ant-inputs]'))
|
|
708
|
+
.find(g => !g.classList.contains('hidden') && g.style.display !== 'none');
|
|
709
|
+
|
|
710
|
+
const container = visibleGroup || this.shadowRoot;
|
|
711
|
+
container.querySelectorAll('[data-ant-input]').forEach(el => {
|
|
712
|
+
this._values[el.dataset.antInput] = el.value;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
_bindEvents() {
|
|
717
|
+
const shadow = this.shadowRoot;
|
|
718
|
+
|
|
719
|
+
// Function tabs
|
|
720
|
+
shadow.querySelectorAll('[data-ant-function]').forEach(el => {
|
|
721
|
+
el.addEventListener('click', () => this._selectFunction(el.dataset.antFunction));
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Input changes
|
|
725
|
+
shadow.querySelectorAll('[data-ant-input]').forEach(el => {
|
|
726
|
+
el.addEventListener('input', (e) => {
|
|
727
|
+
const name = e.target.dataset.antInput;
|
|
728
|
+
this._values[name] = e.target.value;
|
|
729
|
+
|
|
730
|
+
// Update display
|
|
731
|
+
const display = shadow.querySelector(`[data-ant-display="${name}"]`);
|
|
732
|
+
if (display) display.textContent = e.target.value;
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Execute
|
|
737
|
+
shadow.querySelectorAll('[data-ant-execute]').forEach(el => {
|
|
738
|
+
el.addEventListener('click', () => this.execute());
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Keyboard shortcut
|
|
742
|
+
shadow.addEventListener('keydown', (e) => {
|
|
743
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
744
|
+
this.execute();
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
_selectFunction(funcName) {
|
|
750
|
+
this._selectedFunction = funcName;
|
|
751
|
+
|
|
752
|
+
// Update tabs
|
|
753
|
+
this.shadowRoot.querySelectorAll('[data-ant-function]').forEach(el => {
|
|
754
|
+
el.classList.toggle('active', el.dataset.antFunction === funcName);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Define function groups (which functions share inputs)
|
|
758
|
+
const sliderFuncs = ['fibonacci', 'fibonacci_checked', 'fibonacci_sum'];
|
|
759
|
+
const numberFuncs = ['is_fibonacci', 'fibonacci_index'];
|
|
760
|
+
|
|
761
|
+
// Show/hide input groups
|
|
762
|
+
this.shadowRoot.querySelectorAll('[data-ant-inputs]').forEach(group => {
|
|
763
|
+
const forFunc = group.dataset.antInputs;
|
|
764
|
+
|
|
765
|
+
let shouldShow = false;
|
|
766
|
+
|
|
767
|
+
if (forFunc === 'all') {
|
|
768
|
+
shouldShow = true;
|
|
769
|
+
} else if (forFunc === funcName) {
|
|
770
|
+
shouldShow = true;
|
|
771
|
+
} else if (forFunc === 'fibonacci' && sliderFuncs.includes(funcName)) {
|
|
772
|
+
// fibonacci group is shared by all slider functions
|
|
773
|
+
shouldShow = true;
|
|
774
|
+
} else if (forFunc === 'is_fibonacci' && numberFuncs.includes(funcName)) {
|
|
775
|
+
// is_fibonacci group is shared by all number functions
|
|
776
|
+
shouldShow = true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
group.classList.toggle('hidden', !shouldShow);
|
|
780
|
+
group.style.display = shouldShow ? '' : 'none';
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Re-collect values
|
|
784
|
+
this._collectValues();
|
|
785
|
+
|
|
786
|
+
// Hide result
|
|
787
|
+
this._hideResult();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_updateStatus(status) {
|
|
791
|
+
const dot = this.shadowRoot.querySelector('[data-ant-status-dot]');
|
|
792
|
+
if (dot) {
|
|
793
|
+
dot.className = 'status-dot ' + status;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const text = this.shadowRoot.querySelector('[data-ant-status-text]');
|
|
797
|
+
if (text) {
|
|
798
|
+
const labels = { loading: 'Loading...', ready: 'Ready', executing: 'Running...', error: 'Error' };
|
|
799
|
+
text.textContent = labels[status] || status;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
804
|
+
// ⚡ EXECUTION
|
|
805
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
806
|
+
|
|
807
|
+
async execute() {
|
|
808
|
+
if (this._state === 'executing') return;
|
|
809
|
+
|
|
810
|
+
this._state = 'executing';
|
|
811
|
+
this._updateStatus('executing');
|
|
812
|
+
this._setLoading(true);
|
|
813
|
+
|
|
814
|
+
const startTime = performance.now();
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const func = this._wasmInstance.exports[this._selectedFunction];
|
|
818
|
+
if (!func) throw new Error(`Function '${this._selectedFunction}' not found`);
|
|
819
|
+
|
|
820
|
+
const args = this._prepareArgs();
|
|
821
|
+
|
|
822
|
+
console.log(`🐜 Execute: ${this._selectedFunction}(${args.map(a => typeof a === 'bigint' ? a.toString() + 'n' : a).join(', ')})`);
|
|
823
|
+
|
|
824
|
+
let result = func(...args);
|
|
825
|
+
if (typeof result === 'bigint') result = result.toString();
|
|
826
|
+
|
|
827
|
+
const time = (performance.now() - startTime).toFixed(2);
|
|
828
|
+
|
|
829
|
+
this._showResult(result, time, false);
|
|
830
|
+
|
|
831
|
+
this.dispatchEvent(new CustomEvent('ant-result', {
|
|
832
|
+
detail: { name: this.name, function: this._selectedFunction, result, time, args: this._values }
|
|
833
|
+
}));
|
|
834
|
+
|
|
835
|
+
} catch (err) {
|
|
836
|
+
console.error('🐜 Execution error:', err);
|
|
837
|
+
const time = (performance.now() - startTime).toFixed(2);
|
|
838
|
+
this._showResult(err.message, time, true);
|
|
839
|
+
|
|
840
|
+
this.dispatchEvent(new CustomEvent('ant-error', {
|
|
841
|
+
detail: { name: this.name, function: this._selectedFunction, error: err.message }
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
this._state = 'ready';
|
|
846
|
+
this._updateStatus('ready');
|
|
847
|
+
this._setLoading(false);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
_prepareArgs() {
|
|
851
|
+
const args = [];
|
|
852
|
+
|
|
853
|
+
// Find visible input group
|
|
854
|
+
const visibleGroup = Array.from(this.shadowRoot.querySelectorAll('[data-ant-inputs]'))
|
|
855
|
+
.find(g => !g.classList.contains('hidden') && g.style.display !== 'none');
|
|
856
|
+
|
|
857
|
+
const container = visibleGroup || this.shadowRoot;
|
|
858
|
+
|
|
859
|
+
container.querySelectorAll('[data-ant-input]').forEach(el => {
|
|
860
|
+
const value = el.value;
|
|
861
|
+
const type = (el.dataset.antType || 'i32').toLowerCase();
|
|
862
|
+
|
|
863
|
+
if (type === 'i64' || type === 'u64') {
|
|
864
|
+
args.push(BigInt(parseInt(value, 10) || 0));
|
|
865
|
+
} else if (type === 'f32' || type === 'f64') {
|
|
866
|
+
args.push(parseFloat(value) || 0);
|
|
867
|
+
} else if (type === 'bool') {
|
|
868
|
+
args.push(el.checked || value === 'true' || value === '1');
|
|
869
|
+
} else {
|
|
870
|
+
args.push(parseInt(value, 10) || 0);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return args;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
_setLoading(loading) {
|
|
878
|
+
this.shadowRoot.querySelectorAll('[data-ant-execute]').forEach(btn => {
|
|
879
|
+
btn.disabled = loading;
|
|
880
|
+
btn.classList.toggle('loading', loading);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
_showResult(value, time, isError) {
|
|
885
|
+
const container = this.shadowRoot.querySelector('[data-ant-output]');
|
|
886
|
+
if (container) {
|
|
887
|
+
container.classList.add('visible');
|
|
888
|
+
container.classList.toggle('error', isError);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const valueEl = this.shadowRoot.querySelector('[data-ant-value]');
|
|
892
|
+
if (valueEl) {
|
|
893
|
+
valueEl.textContent = this._formatValue(value, isError);
|
|
894
|
+
valueEl.className = 'result-value';
|
|
895
|
+
|
|
896
|
+
if (isError) {
|
|
897
|
+
valueEl.classList.add('error');
|
|
898
|
+
} else if (this._isBoolFunction()) {
|
|
899
|
+
const isTruthy = value === 1 || value === '1' || value === true;
|
|
900
|
+
valueEl.classList.add(isTruthy ? 'bool-true' : 'bool-false');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const timeEl = this.shadowRoot.querySelector('[data-ant-time]');
|
|
905
|
+
if (timeEl) timeEl.textContent = `⚡ ${time}ms`;
|
|
906
|
+
|
|
907
|
+
const exprEl = this.shadowRoot.querySelector('[data-ant-expr]');
|
|
908
|
+
if (exprEl) {
|
|
909
|
+
const argsStr = Object.values(this._values).join(', ');
|
|
910
|
+
exprEl.textContent = `${this._selectedFunction}(${argsStr})`;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
_hideResult() {
|
|
915
|
+
const container = this.shadowRoot.querySelector('[data-ant-output]');
|
|
916
|
+
if (container) {
|
|
917
|
+
container.classList.remove('visible', 'error');
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
_formatValue(value, isError) {
|
|
922
|
+
if (isError) return `Error: ${value}`;
|
|
923
|
+
|
|
924
|
+
if (this._isBoolFunction()) {
|
|
925
|
+
const trueLabel = this.shadowRoot.querySelector('[data-ant-true-label]')?.textContent || '✅ True';
|
|
926
|
+
const falseLabel = this.shadowRoot.querySelector('[data-ant-false-label]')?.textContent || '❌ False';
|
|
927
|
+
const isTruthy = value === 1 || value === '1' || value === true;
|
|
928
|
+
return isTruthy ? trueLabel : falseLabel;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Locale formatting for numbers
|
|
932
|
+
try {
|
|
933
|
+
return BigInt(value).toLocaleString();
|
|
934
|
+
} catch {
|
|
935
|
+
return Number(value).toLocaleString();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
_isBoolFunction() {
|
|
940
|
+
const boolPrefixes = ['is_', 'has_', 'can_', 'should_', 'check_'];
|
|
941
|
+
return boolPrefixes.some(p => this._selectedFunction.startsWith(p));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
945
|
+
// 🔌 PUBLIC API
|
|
946
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
947
|
+
|
|
948
|
+
getFunctions() {
|
|
949
|
+
if (!this._wasmInstance) return [];
|
|
950
|
+
return Object.keys(this._wasmInstance.exports)
|
|
951
|
+
.filter(k => typeof this._wasmInstance.exports[k] === 'function' && !k.startsWith('_'));
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
setValues(values) {
|
|
955
|
+
Object.assign(this._values, values);
|
|
956
|
+
for (const [key, val] of Object.entries(values)) {
|
|
957
|
+
const el = this.shadowRoot.querySelector(`[data-ant-input="${key}"]`);
|
|
958
|
+
if (el) el.value = val;
|
|
959
|
+
const display = this.shadowRoot.querySelector(`[data-ant-display="${key}"]`);
|
|
960
|
+
if (display) display.textContent = val;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
getValues() { return { ...this._values }; }
|
|
965
|
+
|
|
966
|
+
setFunction(name) { this._selectFunction(name); }
|
|
967
|
+
|
|
968
|
+
getFunction() { return this._selectedFunction; }
|
|
969
|
+
|
|
970
|
+
async invoke(funcName, values) {
|
|
971
|
+
if (funcName) this._selectFunction(funcName);
|
|
972
|
+
if (values) this.setValues(values);
|
|
973
|
+
await this.execute();
|
|
974
|
+
return this.shadowRoot.querySelector('[data-ant-value]')?.textContent;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async clearCache() {
|
|
978
|
+
try {
|
|
979
|
+
const db = await this._openDB();
|
|
980
|
+
await Promise.all(['modules', 'ui', 'packages'].map(store =>
|
|
981
|
+
new Promise((resolve, reject) => {
|
|
982
|
+
const tx = db.transaction(store, 'readwrite');
|
|
983
|
+
const req = tx.objectStore(store).delete(this.name);
|
|
984
|
+
req.onsuccess = resolve;
|
|
985
|
+
req.onerror = reject;
|
|
986
|
+
})
|
|
987
|
+
));
|
|
988
|
+
console.log(`🐜 Cache cleared for ${this.name}`);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
console.warn('🐜 Failed to clear cache:', e);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
customElements.define('ant-module', AntModule);
|
|
996
|
+
|
|
997
|
+
export { AntModule };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@banastechnologie/antcloud-sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "🐜 AntCloud WebComponents SDK - Robust WASM module loading with caching",
|
|
5
|
+
"main": "ant-module.js",
|
|
6
|
+
"module": "ant-module.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./ant-module.js",
|
|
11
|
+
"default": "./ant-module.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"ant-module.js",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/yannbanas/antcloud-sdk.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"wasm",
|
|
27
|
+
"webassembly",
|
|
28
|
+
"webcomponents",
|
|
29
|
+
"custom-elements",
|
|
30
|
+
"antcloud",
|
|
31
|
+
"anthive",
|
|
32
|
+
"serverless",
|
|
33
|
+
"micro-frontend",
|
|
34
|
+
"sdk"
|
|
35
|
+
],
|
|
36
|
+
"author": "banastechnologie",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/yannbanas/antcloud-sdk/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/yannbanas/antcloud-sdk#readme"
|
|
42
|
+
}
|