@jseeio/jsee 0.2.8 → 0.3.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/src/utils.js CHANGED
@@ -1,21 +1,92 @@
1
- function getModelFuncJS (model, target, log=console.log) {
1
+ // https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
2
+ function isObject (item) {
3
+ return (typeof item === 'object' && !Array.isArray(item) && item !== null)
4
+ }
5
+
6
+ async function getModelFuncJS (model, target, app) {
7
+ let modelFunc
2
8
  switch (model.type) {
3
9
  case 'class':
4
- log('Init class')
10
+ app.log('Init class')
5
11
  const modelClass = new target()
6
- return (...a) => {
12
+ modelFunc = (...a) => {
7
13
  return modelClass[model.method || 'predict'](...a)
8
14
  }
15
+ break
9
16
  case 'async-init':
10
- // TODO: Test this
11
- log('Function with async init')
12
- return target().then(m => {
13
- log('> Async init resolved: ', m)
14
- return m
15
- })
17
+ app.log('Function with async init')
18
+ modelFunc = await target()
19
+ break
16
20
  default:
17
- log('Init function')
18
- return target
21
+ app.log('Init function')
22
+ modelFunc = target
23
+ }
24
+
25
+ // Wrap modelFunc to take into account container
26
+ // Possible cases:
27
+ if (model.container === 'args') {
28
+ return (...a) => {
29
+ if (Array.isArray(a[0]) && a[0].length && a.length === 1) {
30
+ return modelFunc(...a[0])
31
+ } else if (isObject(a[0]) && a.length === 1) {
32
+ return modelFunc(...Object.values(a[0]))
33
+ } else {
34
+ return modelFunc(...a)
35
+ }
36
+ }
37
+ } else {
38
+ return (...a) => {
39
+ if (isObject(a[0]) && a.length === 1) {
40
+ // In case when we have only one input object
41
+ // Pass log and callback to the model function
42
+ return modelFunc(a[0], app)
43
+ } else {
44
+ return modelFunc(...a)
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ function getUrl (url) {
51
+ let newUrl
52
+ try {
53
+ newUrl = (new URL(url)).href
54
+ } catch (e) {
55
+ newUrl = (new URL(url, 'https://cdn.jsdelivr.net/npm/')).href
56
+ }
57
+ return newUrl
58
+ }
59
+
60
+ function importScriptAsync (url, async=true) {
61
+ url = getUrl(url)
62
+ return new Promise((resolve, reject) => {
63
+ try {
64
+ const scriptElement = document.createElement('script')
65
+ scriptElement.type = 'text/javascript'
66
+ scriptElement.async = async
67
+ scriptElement.src = url
68
+ scriptElement.addEventListener('load', (ev) => {
69
+ resolve({ status: true })
70
+ })
71
+ scriptElement.addEventListener('error', (ev) => {
72
+ reject({
73
+ status: false,
74
+ message: `Failed to import ${url}`
75
+ })
76
+ })
77
+ document.body.appendChild(scriptElement);
78
+ } catch (error) {
79
+ reject(error)
80
+ }
81
+ })
82
+ }
83
+
84
+ async function importScripts (...imports) {
85
+ // Load scripts in parallel
86
+ // return Promise.all(imports.map(importScriptAsync))
87
+ // Load scripts in sequence. Possible ordering issues.
88
+ for (const scriptUrl of imports) {
89
+ await importScriptAsync(scriptUrl);
19
90
  }
20
91
  }
21
92
 
@@ -46,7 +117,15 @@ function getModelFuncAPI (model, log=console.log) {
46
117
  }
47
118
  }
48
119
 
120
+ async function delay (ms) {
121
+ return new Promise(resolve => setTimeout(resolve, ms || 1))
122
+ }
123
+
49
124
  module.exports = {
125
+ isObject,
50
126
  getModelFuncJS,
51
- getModelFuncAPI
127
+ getModelFuncAPI,
128
+ importScripts,
129
+ getUrl,
130
+ delay
52
131
  }
package/src/worker.js CHANGED
@@ -6,20 +6,47 @@ function log () {
6
6
  postMessage({_status: 'log', _log: args})
7
7
  }
8
8
 
9
+ function progress (value) {
10
+ postMessage({_status: 'progress', _progress: value})
11
+ }
12
+
9
13
  function initTF (model) {
10
14
  throw new Error('Tensorflow in worker (not implemented)')
11
15
  }
12
16
 
13
- function initPython (model) {
14
- throw new Error('Python in worker (not implemented)')
17
+ async function initPython (model) {
18
+ importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js")
19
+ const pyodide = await loadPyodide()
20
+ if (model.imports && Array.isArray(model.imports) && model.imports.length) {
21
+ await pyodide.loadPackage(model.imports)
22
+ } else {
23
+ await pyodide.loadPackagesFromImports(model.code)
24
+ }
25
+ return async (data) => {
26
+ for (let key in data) {
27
+ self[key] = data[key]
28
+ }
29
+ return await pyodide.runPythonAsync(model.code);
30
+ }
15
31
  }
16
32
 
17
- function initJS (model) {
33
+ async function initJS (model) {
18
34
  log('Init JS')
19
35
  this.container = model.container
20
36
 
37
+ // Load imports
38
+ if (model.imports && model.imports.length) {
39
+ log('Loading imports...')
40
+ for (let imp of model.imports) {
41
+ // Try creating an url
42
+ const url = utils.getUrl(imp)
43
+ log('Importing:', url)
44
+ importScripts(url)
45
+ }
46
+ }
47
+
21
48
  if (model.code) {
22
- log('Load code as a string')
49
+ log('Load code as a string', model)
23
50
  // https://github.com/altbdoor/blob-worker/blob/master/blobWorker.js
24
51
  importScripts(URL.createObjectURL(new Blob([model.code], { type: 'text/javascript' })))
25
52
  } else if (model.url) {
@@ -29,92 +56,61 @@ function initJS (model) {
29
56
  log('No script provided')
30
57
  }
31
58
 
32
-
33
59
  // Related:
34
60
  // https://stackoverflow.com/questions/37711603/javascript-es6-class-definition-not-accessible-in-window-global
35
- const target = model.type === 'class'
61
+ const target = model.type === 'class'
36
62
  ? eval(model.name)
37
63
  : this[model.name]
64
+
38
65
  // Need promise here in case of async init
39
- Promise.resolve(utils.getModelFuncJS(model, target, log))
40
- .then(m => {
41
- postMessage({_status: 'loaded'})
42
- this.modelFunc = m
43
- })
66
+ let modelFunc = await utils.getModelFuncJS(model, target, { log, progress })
67
+
68
+ return modelFunc
44
69
  }
45
70
 
46
71
  function initAPI (model) {
47
72
  log('Init API')
48
- this.modelFunc = utils.getModelFuncAPI(model, log)
73
+ return utils.getModelFuncAPI(model, log)
49
74
  }
50
75
 
51
- onmessage = function (e) {
52
-
76
+ onmessage = async function (e) {
53
77
  var data = e.data
54
78
  log('Received message of type:', typeof data)
55
79
 
56
80
  if ((typeof data === 'object') && ((data.url) || (data.code))) {
57
- /*
58
- INIT MESSAGE
59
- */
60
- let model = data
81
+ // Init message
61
82
  log('Init...')
62
-
63
- switch (model.type) {
83
+ let m = data
84
+ switch (m.type) {
64
85
  case 'tf':
65
- initTF(model)
86
+ self.modelFunc = await initTF(m)
66
87
  break
67
88
  case 'py':
68
- initPython(model)
89
+ self.modelFunc = await initPython(m)
69
90
  break
70
91
  case 'function':
71
92
  case 'class':
72
93
  case 'async-init':
73
94
  case 'async-function':
74
- initJS(model)
95
+ self.modelFunc = await initJS(m)
75
96
  break
76
97
  case 'get':
77
98
  case 'post':
78
- initAPI(model)
99
+ self.modelFunc = await initAPI(m)
79
100
  break
101
+ default:
102
+ throw new Error(`No type information: ${m.type}`)
80
103
  }
104
+ postMessage({_status: 'loaded'})
81
105
  } else {
82
- /*
83
- :w
84
- CALL MESSAGE
85
- */
86
- var res
87
- if (typeof this.modelFunc === 'string') {
88
- // Python model:
89
- log('Calling Python model')
90
- /*
91
- const keys = Object.keys(data)
92
- for (let key of keys) {
93
- self[key] = data[key];
94
- }
95
- self.pyodide.runPythonAsync(this.model, () => {})
96
- .then((res) => {
97
- console.log('[Worker] Py results: ', typeof res, res)
98
- postMessage(res)
99
- })
100
- .catch((err) => {
101
- // self.postMessage({error : err.message});
102
- })
103
- */
104
- } else {
105
- // JavaScript model
106
- log('Calling JavaScript model')
107
- if (this.container === 'args') {
108
- log('Applying inputs as arguments')
109
- res = this.modelFunc.apply(null, data)
110
- } else {
111
- // JS object or array
112
- log('Applying inputs as object/array')
113
- res = this.modelFunc(data, log)
114
- }
115
- // Return promise value or just regular value
116
- // Promise.resolve handles both cases
117
- Promise.resolve(res).then(r => { postMessage(r) })
106
+ // Execution
107
+ try {
108
+ log('Run model with data:', data)
109
+ const results = await self.modelFunc(data)
110
+ log('Results:', results)
111
+ postMessage(results)
112
+ } catch (error) {
113
+ postMessage({ _status: 'error', _error: error })
118
114
  }
119
115
  }
120
116
  }
@@ -85,7 +85,7 @@
85
85
  justify-content: right;
86
86
  }
87
87
 
88
- .card-footer .run-button:hover,{
88
+ .card-footer .run-button:hover {
89
89
  background-color: transparent !important;
90
90
  background: linear-gradient(270deg, #02dbb2 0%, #fff0 80%);
91
91
  box-shadow: 5px 0px 5px -2px #48ffd43b;
@@ -95,6 +95,13 @@
95
95
  color: #016c5c !important;
96
96
  }
97
97
 
98
+ .example-button {
99
+ margin-top: 3px;
100
+ padding: 5px 10px;
101
+ border-radius: 5px !important;
102
+ height: auto;
103
+ }
104
+
98
105
  .field {
99
106
  margin-bottom: 5px;
100
107
  }
@@ -129,20 +136,25 @@
129
136
  <p v-if="$parent.model.description">{{ $parent.model.description }}</p>
130
137
  </div>
131
138
  </div>
132
- <div class="columns">
139
+ <div class="columns is-multiline">
133
140
  <div class="column" v-bind:class="($parent.design && $parent.design.grid && ($parent.design.grid.length > 0)) ? 'is-' + $parent.design.grid[0] : ''">
141
+ <!-- Inputs -->
134
142
  <div class="card bordered">
135
143
  <div class="card-content" id="inputs" v-if="$parent.inputs && $parent.inputs.length > 0">
136
144
  <ul>
137
145
  <li v-for="(input, index) in $parent.inputs">
138
- <vue-input v-bind:input="input" v-if="$parent.display(index)" v-on:inchange="$parent.run()"></vue-input>
146
+ <vue-input
147
+ v-bind:input="input"
148
+ v-if="input.display !== false && $parent.display(index)"
149
+ v-on:inchange="$parent.run()"
150
+ ></vue-input>
139
151
  </li>
140
152
  </ul>
141
153
  <pre v-if="$parent.model.debug">{{ $parent.inputs }}</pre>
142
154
  <!-- <button class="button is-primary" id="run"><span>▸</span>&nbsp;&nbsp;Run</button> -->
143
155
  </div>
144
156
  <footer class="card-footer" v-bind:class="{ reset: $parent.dataChanged }">
145
- <button
157
+ <button
146
158
  v-on:click="$parent.reset()"
147
159
  v-if="$parent.inputs && $parent.inputs.length > 0 && $parent.dataChanged"
148
160
  class="button reset-button icon card-footer-item is-danger is-small"
@@ -150,8 +162,8 @@
150
162
  <span class="rest-icon has-text-danger-dark">✕</span>
151
163
  <span>&nbsp; Reset</span>
152
164
  </button>
153
- <button
154
- v-on:click="$parent.run()"
165
+ <button
166
+ v-on:click="$parent.run('run')"
155
167
  class="button run-button icon card-footer-item is-primary is-small"
156
168
  v-bind:class="{ running: $parent.clickRun }"
157
169
  >
@@ -160,8 +172,21 @@
160
172
  </button>
161
173
  </footer>
162
174
  </div>
175
+ <!-- Examples -->
176
+ <div v-if="$parent.examples">
177
+ <p style="margin-top: 20px">Examples</p>
178
+ <div v-for="(example, index) in $parent.examples">
179
+ <button
180
+ v-on:click="$parent.reset(example)"
181
+ class="button is-small example-button"
182
+ >
183
+ {{ example }}
184
+ </button>
185
+ </div>
186
+ </div>
163
187
  </div>
164
- <div class="column" id="outputs">
188
+ <div class="column" id="outputs" v-bind:class="($parent.design && $parent.design.grid && ($parent.design.grid.length > 1)) ? 'is-' + $parent.design.grid[1] : ''">
189
+ <!-- Outputs -->
165
190
  <div v-if="$parent.outputs">
166
191
  <div v-for="(output, index) in $parent.outputs">
167
192
  <vue-output v-bind:output="output" v-on:notification="$parent.notify($event)"></vue-output>
@@ -85,12 +85,8 @@
85
85
  type="file"
86
86
  >
87
87
  <span class="file-cta">
88
- <span class="file-icon">
89
- <span v-if="input.file && input.file.name">✓</span>
90
- <span v-else>↥</span>
91
- </span>
92
88
  <span class="file-label">
93
- Choose a file…
89
+ Open
94
90
  </span>
95
91
  </span>
96
92
  <span class="file-name">
@@ -102,6 +98,16 @@
102
98
  </div>
103
99
  </div>
104
100
 
101
+ <div class="field" v-if="input.type == 'action' || input.type == 'button'">
102
+ <button
103
+ v-on:click="$parent.$parent.run(input.name.toLowerCase().replace(/ /g, '_'))"
104
+ class="button is-small"
105
+ style="width: 100%; padding: 5px 0; margin-top: 8px; text-align: left;"
106
+ >
107
+ <span>&nbsp; {{ input.title ? input.title : input.name }} &nbsp;</span>
108
+ </button>
109
+ </div>
110
+
105
111
  <div class="field is-horizontal" v-if="input.type == 'group'">
106
112
  <div class="field-body">
107
113
  <vue-input v-for="(el, index) in input.elements" v-bind:input="el" ></vue-input>
@@ -21,6 +21,9 @@ const component = {
21
21
  this.$emit('inchange')
22
22
  }
23
23
  }
24
+ },
25
+ call (method) {
26
+ console.log('calling: ', method)
24
27
  }
25
28
  }
26
29
  }
@@ -0,0 +1,22 @@
1
+
2
+ <html>
3
+ <div id="jsee-container">
4
+ <script src="/dist/jsee.js"></script>
5
+ <script>
6
+ new JSEE({
7
+ schema: {
8
+ 'model': {
9
+ 'name': 'Doubler',
10
+ 'method': 'double',
11
+ 'type': 'class',
12
+ 'container': 'args',
13
+ 'url': '/test/example-class.js',
14
+ },
15
+ 'inputs': [
16
+ { 'name': 'b', 'type': 'int', 'default': 100 }
17
+ ]
18
+ },
19
+ container: '#jsee-container'
20
+ })
21
+ </script>
22
+ </html>
@@ -0,0 +1,28 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.js"></script>
4
+ <script>
5
+
6
+ new JSEE({
7
+ schema: {
8
+ "model": {
9
+ "name": "kebab",
10
+ "type": "function",
11
+ "container": "args",
12
+ "code": `
13
+ function kebab (str) {
14
+ return _.kebabCase(str)
15
+ }
16
+ `,
17
+ "worker": false,
18
+ },
19
+ "imports": "lodash@4.17.21/lodash.min.js",
20
+ "inputs": [
21
+ { "name": "str", "type": "string", "default": 'FooBar' },
22
+ ]
23
+ },
24
+ container: '#jsee-container',
25
+ verbose: true
26
+ })
27
+ </script>
28
+ </html>
package/test/minimal.html CHANGED
@@ -8,7 +8,7 @@
8
8
  return a * b
9
9
  }
10
10
  },
11
- container: 'jsee-container'
11
+ container: '#jsee-container'
12
12
  })
13
13
  </script>
14
14
  </html>
@@ -6,7 +6,7 @@
6
6
  schema: {
7
7
  "model": {
8
8
  "name": "div",
9
- "code": "function div (a) { ruturn a / 2 }"
9
+ "code": "function div (a) { return a / 2 }"
10
10
  },
11
11
  "inputs": [
12
12
  {
@@ -0,0 +1,52 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.js"></script>
4
+ <script>
5
+ new JSEE({
6
+ schema: {
7
+ "model": [
8
+ {
9
+ "name": "sum",
10
+ "type": "function",
11
+ "container": "args",
12
+ "code": function sum (a, b) {
13
+ return a + b
14
+ },
15
+ "worker": true
16
+ },
17
+ {
18
+ "name": "square",
19
+ "type": "function",
20
+ "container": "args",
21
+ "code": function square (a) {
22
+ return a * a
23
+ }
24
+ },
25
+ {
26
+ "name": "Increment",
27
+ "type": "class",
28
+ "method": "step",
29
+ "container": "args",
30
+ "code": class Increment {
31
+ constructor () {
32
+ this.i = 0
33
+ }
34
+ step (a) {
35
+ this.i += 1
36
+ return a + this.i
37
+ }
38
+ }
39
+ }
40
+ ],
41
+ "inputs": [
42
+ { "name": "a", "type": "int", "default": 2 },
43
+ { "name": "b", "type": "int", "default": 3 }
44
+ ],
45
+ "outputs": [
46
+ { "name": "square", "type": "int" }
47
+ ],
48
+ "autorun": false
49
+ }
50
+ })
51
+ </script>
52
+ </html>
@@ -0,0 +1,41 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.js"></script>
4
+ <script>
5
+ new JSEE({
6
+ schema: {
7
+ "model": {
8
+ "name": "pytest",
9
+ "type": "py",
10
+ "description": "Testing pyodide project",
11
+ "container": "object",
12
+ "code": `
13
+ import numpy as np
14
+ from js import a, b
15
+ np.mean([a, b])
16
+ `.replace(/^\s+/gm, ''),
17
+ "autorun": false,
18
+ "worker": true,
19
+ "imports": [
20
+ "numpy"
21
+ ]
22
+ },
23
+ "inputs": [
24
+ {
25
+ "type": "int",
26
+ "name": "a"
27
+ },
28
+ {
29
+ "type": "int",
30
+ "name": "b"
31
+ }
32
+ ],
33
+ "outputs": [
34
+ {
35
+ "type": "int"
36
+ }
37
+ ]
38
+ }
39
+ })
40
+ </script>
41
+ </html>