@jseeio/jsee 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/load.html ADDED
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>JSEE Schema Loader</title>
7
+ </head>
8
+ <body>
9
+ <div id="jsee-container"></div>
10
+ <div id="txt-container">
11
+ <p>You can use this script for testing purposes. Placing your apps in the <b>apps</b> folder (e.g. <code>./apps/myapp</code> will make it possible to load it with <code>load.html?s=myapp</code> </p>
12
+ </div>
13
+ <script src="dist/jsee.js" type="text/javascript"></script>
14
+ <script>
15
+ var params = new URLSearchParams(window.location.search)
16
+ var schema = params.get('s')
17
+ if (schema) {
18
+ if (typeof schema === 'string') {
19
+ schema = schema.includes('/') ? schema : '/apps/' + schema + '/schema.json'
20
+ }
21
+ const env = new JSEE({
22
+ container: document.getElementById('jsee-container'),
23
+ schema: schema
24
+ })
25
+ } else {
26
+ document.getElementById('port-container').style.display = 'none'
27
+ document.getElementById('txt-container').style.display = 'block'
28
+ }
29
+ </script>
30
+ </body>
31
+ </html>
package/main.js ADDED
@@ -0,0 +1,449 @@
1
+ import { createVueApp } from './src/app'
2
+ import Worker from './src/worker.js'
3
+
4
+ const { Notyf } = require('notyf')
5
+ const notyf = new Notyf({
6
+ types: [
7
+ {
8
+ type: 'success',
9
+ background: '#00d1b2',
10
+ },
11
+ {
12
+ type: 'error',
13
+ background: '#f14668',
14
+ duration: 2000,
15
+ dismissible: true
16
+ }
17
+ ]
18
+ })
19
+
20
+ // const createVueApp = require('./src/app')
21
+ const Overlay = require('./src/overlay')
22
+
23
+ require('notyf/notyf.min.css')
24
+
25
+ const fetch = window['fetch']
26
+ const Blob = window['Blob']
27
+
28
+ function log () {
29
+ console.log(`[JSEE v${VERSION}]`, ...arguments)
30
+ }
31
+
32
+ // const Worker = window['Worker']
33
+
34
+ // Deep clone a simple object
35
+ function clone (obj) {
36
+ // return JSON.parse(JSON.stringify(obj))
37
+ return Object.assign({}, obj)
38
+ }
39
+
40
+ // https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
41
+ function isObject (item) {
42
+ return (typeof item === 'object' && !Array.isArray(item) && item !== null)
43
+ }
44
+
45
+ // Return input value
46
+ function getValue (input) {
47
+ if (input.type === 'group') {
48
+ const value = {}
49
+ input.elements.forEach(el => {
50
+ value[el.name] = getValue(el)
51
+ })
52
+ return value
53
+ } else {
54
+ return input.value
55
+ }
56
+ }
57
+
58
+ export default class JSEE {
59
+ constructor (params) {
60
+ log('Initializing JSEE with parameters: ', params)
61
+ params.schema = params.schema || params.config
62
+ this.params = params
63
+ this.__version__ = VERSION
64
+
65
+ // Get schema then initialize a model
66
+ if (params.schema) {
67
+ if (typeof params.schema === 'object') {
68
+ log('Received schema as object')
69
+ this.init(params.schema)
70
+ } else if (typeof params.schema === 'string') {
71
+ log('Received schema as string')
72
+ this.schemaUrl = params.schema.indexOf('json') ? params.schema : params.schema + '.json'
73
+ fetch(this.schemaUrl)
74
+ .then(res => res.json())
75
+ .then(res => {
76
+ log('Loaded schema from url')
77
+ this.init(res)
78
+ })
79
+ .catch((err) => {
80
+ console.error(err)
81
+ })
82
+ }
83
+ }
84
+ }
85
+
86
+ notify (txt) {
87
+ notyf.success(txt)
88
+ }
89
+
90
+ // Initialize model from schema
91
+ init (schema) {
92
+ log('Initializing schema', schema)
93
+
94
+ // Convert JS code to string
95
+ if (schema.model.code && (typeof schema.model.code !== 'string')) {
96
+ log('Convert code in schema to string')
97
+ schema.model.code = schema.model.code.toString()
98
+ }
99
+
100
+ // Update model URL if needed
101
+ if (schema.model.url && !schema.model.url.includes('/') && this.schemaUrl && this.schemaUrl.includes('/')) {
102
+ let oldModelUrl = schema.model.url
103
+ log('Schema URL:', this.schemaUrl)
104
+ schema.model.url = window.location.protocol + '//' + window.location.host + this.schemaUrl.split('/').slice(0, -1).join('/') + '/' + oldModelUrl
105
+ log('Changed the old model URL to absolute one:', oldModelUrl, schema.model.url)
106
+ }
107
+
108
+ // Check for worker flag
109
+ if (typeof schema.model.worker === 'undefined') {
110
+ schema.model.worker = true
111
+ }
112
+
113
+ // Check if name is present, if not - get name from the file
114
+ if (typeof schema.model.name === 'undefined') {
115
+ // Two options here
116
+ if (schema.model.url) {
117
+ // 1. Get the name from the file name
118
+ schema.model.name = schema.model.url.split('/').pop().split('.')[0]
119
+ log('Use name from url: ', schema.model.name)
120
+ } else if (schema.model.code) {
121
+ // 2. Get the name from the url
122
+ schema.model.name = schema.model.code.name
123
+ log('Use name from code: ', schema.model.name)
124
+ }
125
+ }
126
+
127
+ this.schema = clone(schema)
128
+
129
+ // Init Vue app
130
+ this.app = createVueApp(this, this.schema, (container) => {
131
+ // Called when the app is mounted
132
+ // FYI "this" here refers to port object
133
+ this.outputsContainer = container.querySelector('#outputs')
134
+ this.inputsContainer = container.querySelector('#inputs')
135
+ this.modelContainer = container.querySelector('#model')
136
+
137
+ // Init overlay
138
+ this.overlay = new Overlay(this.inputsContainer)
139
+ })
140
+ this.data = this.app.$data
141
+
142
+ // Init Model
143
+ // ----------
144
+ if (this.schema.model.type === 'py') {
145
+ // Add loading indicator
146
+ this.overlay.show()
147
+ let script = document.createElement('script')
148
+ script.src = 'https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js'
149
+ script.onload = async () => {
150
+ this.pyodide = await loadPyodide({ indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/" });
151
+ notyf.success('Loaded: Python')
152
+ const resRaw = await fetch(this.schema.model.url)
153
+ const res = await resRaw.text()
154
+ this.pymodel = res
155
+ log('Loaded python code:', res)
156
+ // Check if micropip is used
157
+ if (res.includes('micropip')) {
158
+ await this.pyodide.loadPackage('micropip')
159
+ log('Loaded micropip')
160
+ }
161
+ // Import packages if defined
162
+ if ('packages' in this.schema.model) {
163
+ await this.pyodide.loadPackage(this.schema.model.packages)
164
+ log('Loaded packages from schema')
165
+ } else {
166
+ await this.pyodide.loadPackagesFromImports(res)
167
+ log('Loaded packages from Python code')
168
+ }
169
+ this.overlay.hide()
170
+ }
171
+ document.head.appendChild(script)
172
+ } else if (['function', 'class', 'async-init', 'async-function'].includes(this.schema.model.type)) {
173
+ // Initialize worker with the model
174
+ if (this.schema.model.worker) {
175
+ // this.worker = new Worker(new URL('./src/worker.js', import.meta.url))
176
+ this.worker = new Worker()
177
+ if (this.schema.model.url) {
178
+ fetch(this.schema.model.url)
179
+ .then(res => res.text())
180
+ .then(res => {
181
+ log('Loaded js code for worker')
182
+ this.schema.model.code = res
183
+ this.worker.postMessage(this.schema.model)
184
+ })
185
+ } else if (typeof this.schema.model.code !== 'undefined') {
186
+ this.worker.postMessage(this.schema.model)
187
+ } else {
188
+ notyf.error('No code provided')
189
+ }
190
+
191
+ this.worker.onmessage = (e) => {
192
+ this.overlay.hide()
193
+ const res = e.data
194
+ if ((typeof res === 'object') && (res._status)) {
195
+ switch (res._status) {
196
+ case 'loaded':
197
+ notyf.success('Loaded: JS model (in worker)')
198
+ break
199
+ case 'log':
200
+ log(...res._log)
201
+ break
202
+ }
203
+ } else {
204
+ log('Response from worker:', res)
205
+ this.output(res)
206
+ }
207
+ }
208
+ this.worker.onerror = (e) => {
209
+ this.overlay.hide()
210
+ notyf.error(e.message)
211
+ log('Error from worker:', e)
212
+ }
213
+ } else {
214
+ // Initialize model in main window
215
+ log('Init model in window')
216
+ let script = document.createElement('script')
217
+ script.src = this.schema.model.url
218
+ script.onload = () => {
219
+ notyf.success('Loaded: JS model')
220
+ this.overlay.hide()
221
+ log('Loaded JS model in main window')
222
+
223
+ // Initializing the model (same in worker)
224
+ if (this.schema.model.type === 'class') {
225
+ log('Init class')
226
+ const modelClass = new window[this.schema.model.name]()
227
+ this.modelFunc = (...a) => {
228
+ return modelClass[this.schema.model.method || 'predict'](...a)
229
+ }
230
+ } else if (this.schema.model.type === 'async-init') {
231
+ log('Init function with promise')
232
+ window[this.schema.model.name]().then((m) => {
233
+ log('Async init resolved: ', m)
234
+ this.modelFunc = m
235
+ })
236
+ } else {
237
+ log('Init function')
238
+ this.modelFunc = window[this.schema.model.name]
239
+ }
240
+ }
241
+ document.head.appendChild(script)
242
+ }
243
+ } else if (this.schema.model.type === 'tf') {
244
+ // Initialize TF
245
+ let script = document.createElement('script')
246
+ script.src = 'dist/tf.min.js'
247
+ script.onload = () => {
248
+ log('Loaded TF.js')
249
+ this.overlay.hide()
250
+ window['tf'].loadLayersModel(this.schema.model.url).then(res => {
251
+ log('Loaded Tensorflow model')
252
+ })
253
+ }
254
+ document.head.appendChild(script)
255
+ } else if (this.schema.model.type === 'get') {
256
+ this.overlay.hide()
257
+ this.modelFunc = (data) => {
258
+ const query = new window['URLSearchParams'](data).toString()
259
+ log('Generated query string:', query)
260
+ const resPromise = fetch(this.schema.model.url +'?' + query)
261
+ .then(response => response.json())
262
+ return resPromise
263
+ }
264
+ }
265
+
266
+ // Init render
267
+ // -----------
268
+ if (this.schema.render && this.schema.render.url) {
269
+ log('Init render in window')
270
+ let script = document.createElement('script')
271
+ script.src = this.schema.render.url
272
+ script.onload = () => {
273
+ notyf.success('Loaded: JS render')
274
+ log('Loaded JS render')
275
+
276
+ // Initializing the render (same in worker)
277
+ if (this.schema.render.type === 'class') {
278
+ log('Init render as class')
279
+ const renderClass = new window[this.schema.render.name]()
280
+ this.renderFunc = (...a) => {
281
+ return renderClass[this.schema.render.method || 'render'](...a)
282
+ }
283
+ } else if (this.schema.render.type === 'async-init') {
284
+ log('Init render function with promise')
285
+ window[this.schema.render.name]().then((m) => {
286
+ log('Async rebder init resolved: ', m)
287
+ this.renderFunc = m
288
+ })
289
+ } else {
290
+ log('Init render as function')
291
+ this.renderFunc = window[this.schema.render.name]
292
+ }
293
+ }
294
+ document.head.appendChild(script)
295
+ }
296
+ }
297
+
298
+ run () {
299
+ const schema = this.schema
300
+ const data = this.data
301
+ log('Running the model...')
302
+ // Collect input values
303
+ let inputValues
304
+
305
+ if (schema.model && schema.model.container && schema.model.container === 'args') {
306
+ log('Pass inputs as function arguments')
307
+ inputValues = data.inputs.map(input => getValue(input))
308
+ } else {
309
+ log('Pass inputs in an object')
310
+ inputValues = {}
311
+ data.inputs.forEach(input => {
312
+ if (input.name) {
313
+ inputValues[input.name] = getValue(input)
314
+ }
315
+ })
316
+ }
317
+ log('Input values:', inputValues)
318
+ // We have all input values here, pass them to worker, window.modelFunc or tf
319
+ if (!schema.model.autorun) {
320
+ this.overlay.show()
321
+ }
322
+ switch (schema.model.type) {
323
+ case 'tf':
324
+ break
325
+ case 'py':
326
+ data.inputs.forEach(input => {
327
+ this.pyodide.globals.set(input.name, input.value);
328
+ })
329
+ this.pyodide.runPythonAsync(this.pymodel, () => {})
330
+ .then((res) => {
331
+ if (schema.outputs && schema.outputs.length) {
332
+ const resultObj = {}
333
+ schema.outputs.forEach(output => {
334
+ resultObj[output.name] = this.pyodide.globals.get(output.name).toJs()
335
+ })
336
+ this.output(resultObj)
337
+ } else {
338
+ this.output(res)
339
+ }
340
+ })
341
+ .catch((err) => {
342
+ log(err)
343
+ window['M'].toast({html: 'Error in code'})
344
+ })
345
+ break
346
+
347
+ case 'class':
348
+ case 'function':
349
+ case 'async-init':
350
+ case 'async-function':
351
+ case 'get':
352
+ if (this.schema.model.worker) {
353
+ this.worker.postMessage(inputValues)
354
+ } else {
355
+ // Run in main window
356
+ var res
357
+ if (this.schema.model.container === 'args') {
358
+ res = this.modelFunc.apply(null, inputValues)
359
+ } else {
360
+ log('Applying inputs as object')
361
+ res = this.modelFunc(inputValues)
362
+ }
363
+ log('modelFunc results:', res)
364
+ Promise.resolve(res).then(r => { this.output(r) })
365
+ }
366
+ break
367
+ case 'api':
368
+ break
369
+ }
370
+ }
371
+
372
+ output (res) {
373
+ // TODO: Think about all edge cases
374
+ // * No output field, but reactivity
375
+ this.overlay.hide()
376
+
377
+ if (typeof res === 'undefined') {
378
+ return
379
+ }
380
+
381
+ log('Got output results of type:', typeof res)
382
+
383
+ // Process results (res)
384
+ const inputNames = this.schema.inputs.map(i => i.name)
385
+ if (isObject(res) && Object.keys(res).every(key => inputNames.includes(key))) {
386
+ // Update inputs from results
387
+ log('Updating inputs:', Object.keys(res))
388
+ this.data.inputs.forEach((input, i) => {
389
+ if (input.name && (typeof res[input.name] !== 'undefined')) {
390
+ log('Updating input: ', input.name, 'with data:', res[input.name])
391
+ const r = res[input.name]
392
+ if (typeof r === 'object') {
393
+ Object.keys(r).forEach(k => {
394
+ input[k] = r[k]
395
+ })
396
+ } else {
397
+ input.value = r
398
+ }
399
+ }
400
+ })
401
+ } else if (this.renderFunc) {
402
+ // Pass results to a custom render function
403
+ log('Calling a render function...')
404
+ this.renderFunc(res)
405
+ } else if (Array.isArray(res) && res.length) {
406
+ // Result is array
407
+ if (this.data.outputs && this.data.outputs.length) {
408
+ // We have outputs defined
409
+ if (this.data.outputs.length === res.length) {
410
+ // Same length
411
+ this.data.outputs.forEach((output, i) => {
412
+ output.value = res[i]
413
+ })
414
+ } else {
415
+ // Different length
416
+ this.data.outputs[0].value = res
417
+ }
418
+ } else {
419
+ // Outputs are not defined
420
+ this.data.outputs = [{
421
+ 'type': 'array',
422
+ 'value': res
423
+ }]
424
+ }
425
+ } else if (typeof res === 'object') {
426
+ if (this.data.outputs && this.data.outputs.length) {
427
+ this.data.outputs.forEach((output, i) => {
428
+ if (output.name && (typeof res[output.name] !== 'undefined')) {
429
+ log('Updating output: ', output.name)
430
+ output.value = res[output.name]
431
+ }
432
+ })
433
+ } else {
434
+ this.data.outputs = [{
435
+ 'type': 'object',
436
+ 'value': res
437
+ }]
438
+ }
439
+ } else if (this.schema.outputs && this.schema.outputs.length === 1) {
440
+ // One output value passed as raw js object
441
+ this.data.outputs[0].value = res
442
+ } else {
443
+ this.data.outputs = [{
444
+ 'type': typeof res,
445
+ 'value': res
446
+ }]
447
+ }
448
+ }
449
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@jseeio/jsee",
3
+ "version": "0.2.1",
4
+ "description": "",
5
+ "main": "dist/jsee.js",
6
+ "private": false,
7
+ "scripts": {
8
+ "build-dev": "webpack --mode=development --progress",
9
+ "build": "webpack --mode=production --progress && npx webpack --mode=production --progress --env RUNTIME",
10
+ "watch": "nodemon --watch . --ignore dist --ext vue,js,css,html --exec 'npm run build-dev'",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "tape test.js | faucet"
13
+ },
14
+ "author": "Anton Zemlyansky",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@mdi/font": "^6.5.95",
18
+ "bulma": "^0.9.3",
19
+ "csv-parse": "^4.6.1",
20
+ "file-saver": "^2.0.2",
21
+ "filtrex": "^2.2.3",
22
+ "notyf": "^3.10.0",
23
+ "vue": "^3.2.23",
24
+ "vue-style-loader": "^4.1.3",
25
+ "vue3-json-viewer": "^1.0.4",
26
+ "vuex": "^4.0.2"
27
+ },
28
+ "devDependencies": {
29
+ "@babel/core": "^7.16.5",
30
+ "@babel/plugin-transform-runtime": "^7.4.4",
31
+ "@babel/preset-env": "^7.16.5",
32
+ "babel-loader": "^8.2.3",
33
+ "css-loader": "^6.5.1",
34
+ "node-sass": "^7.0.0",
35
+ "nodemon": "^2.0.15",
36
+ "puppeteer": "^1.12.2",
37
+ "sass-loader": "^12.4.0",
38
+ "style-loader": "^3.3.1",
39
+ "tape": "^4.9.1",
40
+ "terser-webpack-plugin": "^5.3.0",
41
+ "uglify-es": "^3.3.9",
42
+ "vue-loader": "^17.0.0",
43
+ "vue-template-compiler": "^2.6.14",
44
+ "webpack": "^5.65.0",
45
+ "webpack-cli": "^4.9.1",
46
+ "worker-loader": "^3.0.8"
47
+ },
48
+ "resolutions": {
49
+ "ansi-regex": "5.0.1"
50
+ }
51
+ }