@jseeio/jsee 0.4.1 → 0.8.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/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +583 -55
- package/dist/2b3e1faf89f94a483539.png +0 -0
- package/dist/416d91365b44e4b4f477.png +0 -0
- package/dist/8f2c4d11474275fbc161.png +0 -0
- package/dist/jsee.core.js +1 -0
- package/dist/jsee.full.js +1 -0
- package/dist/jsee.runtime.js +2 -1
- package/package.json +84 -18
- package/src/app.js +130 -33
- package/src/cli.js +474 -64
- package/src/extended-imports.js +11 -0
- package/src/main.js +264 -45
- package/src/overlay.js +26 -1
- package/src/utils.js +390 -12
- package/templates/common-inputs.js +88 -0
- package/templates/common-outputs.js +340 -4
- package/templates/minimal-app.vue +367 -0
- package/templates/minimal-input.vue +573 -0
- package/templates/minimal-output.vue +426 -0
- package/templates/virtual-table.vue +194 -0
- package/.claude/settings.local.json +0 -13
- package/.eslintrc.js +0 -38
- package/AGENTS.md +0 -65
- package/CLAUDE.md +0 -5
- package/CNAME +0 -1
- package/_config.yml +0 -26
- package/dist/jsee.js +0 -1
- package/jest-puppeteer.config.js +0 -14
- package/jest.config.js +0 -8
- package/jest.unit.config.js +0 -8
- package/load/index.html +0 -52
- package/templates/bulma-app.vue +0 -242
- package/templates/bulma-input.vue +0 -125
- package/templates/bulma-output.vue +0 -101
- package/test/class.html +0 -22
- package/test/code.html +0 -25
- package/test/codew.html +0 -25
- package/test/example-class.js +0 -8
- package/test/example-sum.js +0 -3
- package/test/fixtures/lodash-like.js +0 -15
- package/test/fixtures/upload-sample.csv +0 -3
- package/test/importw.html +0 -28
- package/test/minimal.html +0 -14
- package/test/minimal1.html +0 -13
- package/test/minimal2.html +0 -15
- package/test/minimal3.html +0 -10
- package/test/minimal4.html +0 -22
- package/test/pipeline.html +0 -52
- package/test/python.html +0 -41
- package/test/string.html +0 -26
- package/test/stringw.html +0 -29
- package/test/sum.schema.json +0 -17
- package/test/sumw.schema.json +0 -15
- package/test/test-basic.test.js +0 -603
- package/test/test-python.test.js +0 -23
- package/test/unit/cli-fetch.test.js +0 -229
- package/test/unit/utils.test.js +0 -888
- package/webpack.config.js +0 -101
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Extended bundle imports — only loaded when EXTENDED flag is true.
|
|
2
|
+
// These libraries are heavy and not included in the core bundle.
|
|
3
|
+
// Users on the core bundle can load them manually via schema `imports`
|
|
4
|
+
// and they'll be detected via window globals.
|
|
5
|
+
|
|
6
|
+
window.Plot = require('@observablehq/plot')
|
|
7
|
+
window.THREE = require('three')
|
|
8
|
+
window.L = require('leaflet')
|
|
9
|
+
|
|
10
|
+
// Leaflet CSS
|
|
11
|
+
require('leaflet/dist/leaflet.css')
|
package/src/main.js
CHANGED
|
@@ -25,6 +25,10 @@ const Overlay = require('./overlay')
|
|
|
25
25
|
|
|
26
26
|
require('notyf/notyf.min.css')
|
|
27
27
|
|
|
28
|
+
if (typeof EXTENDED !== 'undefined' && EXTENDED) {
|
|
29
|
+
require('./extended-imports')
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
const fetch = window['fetch']
|
|
29
33
|
const Blob = window['Blob']
|
|
30
34
|
|
|
@@ -124,10 +128,6 @@ function collectStreamInputConfig (inputs, config={}) {
|
|
|
124
128
|
return config
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
function getFunctionContainer (target) {
|
|
128
|
-
// Check if the number of parameters is > 1, then 'args'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
131
|
export default class JSEE {
|
|
132
132
|
constructor (params, alt1, alt2) {
|
|
133
133
|
// Check if JSEE was initialized with args rather than with a params object
|
|
@@ -152,6 +152,12 @@ export default class JSEE {
|
|
|
152
152
|
this.__version__ = VERSION
|
|
153
153
|
this.cancelled = false
|
|
154
154
|
this._cancelWorkerRun = null
|
|
155
|
+
this._rejectRun = null
|
|
156
|
+
this._rejectWorkerRun = null
|
|
157
|
+
this._workers = []
|
|
158
|
+
this._pendingRun = null
|
|
159
|
+
this._runToken = null
|
|
160
|
+
this._needsModelReinit = false
|
|
155
161
|
|
|
156
162
|
// Check if schema is provided
|
|
157
163
|
if (typeof this.schema === 'undefined') {
|
|
@@ -171,7 +177,7 @@ export default class JSEE {
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
this.init()
|
|
180
|
+
this._initPromise = this.init()
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
log (...args) {
|
|
@@ -184,16 +190,66 @@ export default class JSEE {
|
|
|
184
190
|
|
|
185
191
|
cancelCurrentRun () {
|
|
186
192
|
log('Stopping current run')
|
|
193
|
+
const cancelError = new Error('Cancelled')
|
|
187
194
|
this.cancelled = true
|
|
195
|
+
this._pendingRun = null
|
|
196
|
+
this._runToken = {}
|
|
197
|
+
if (typeof this._rejectRun === 'function') {
|
|
198
|
+
this._rejectRun(cancelError)
|
|
199
|
+
this._rejectRun = null
|
|
200
|
+
}
|
|
188
201
|
if (typeof this._cancelWorkerRun === 'function') {
|
|
189
202
|
this._cancelWorkerRun()
|
|
190
203
|
}
|
|
204
|
+
// Reject the in-flight worker promise so run() stops awaiting
|
|
205
|
+
if (typeof this._rejectWorkerRun === 'function') {
|
|
206
|
+
this._rejectWorkerRun(cancelError)
|
|
207
|
+
this._rejectWorkerRun = null
|
|
208
|
+
}
|
|
209
|
+
// Terminate all workers to force-stop blocking WASM computations
|
|
210
|
+
const hadWorkers = this._workers.length > 0
|
|
211
|
+
this._workers.forEach(w => {
|
|
212
|
+
try { w.terminate() } catch (e) { /* ignore */ }
|
|
213
|
+
})
|
|
214
|
+
this._workers = []
|
|
215
|
+
this._cancelWorkerRun = null
|
|
216
|
+
// Force model re-init on next run only when worker-backed state was torn down.
|
|
217
|
+
if (hadWorkers) this._needsModelReinit = true
|
|
191
218
|
}
|
|
192
219
|
|
|
193
220
|
isCancelled () {
|
|
194
221
|
return this.cancelled === true
|
|
195
222
|
}
|
|
196
223
|
|
|
224
|
+
destroy () {
|
|
225
|
+
log('Destroying JSEE instance')
|
|
226
|
+
// Cancel any running computation
|
|
227
|
+
this.cancelCurrentRun()
|
|
228
|
+
// Terminate all workers
|
|
229
|
+
this._workers.forEach(w => {
|
|
230
|
+
try { w.terminate() } catch (e) { /* ignore */ }
|
|
231
|
+
})
|
|
232
|
+
this._workers = []
|
|
233
|
+
// Unmount Vue app
|
|
234
|
+
if (this.app && this.app.__vueApp) {
|
|
235
|
+
this.app.__vueApp.unmount()
|
|
236
|
+
}
|
|
237
|
+
// Clean up overlay
|
|
238
|
+
if (this.overlay) {
|
|
239
|
+
this.overlay.hide()
|
|
240
|
+
}
|
|
241
|
+
// Remove progress bar
|
|
242
|
+
const progress = document.querySelector('#progress')
|
|
243
|
+
if (progress) progress.remove()
|
|
244
|
+
// Null out references
|
|
245
|
+
this.app = null
|
|
246
|
+
this.data = null
|
|
247
|
+
this.pipeline = null
|
|
248
|
+
this.model = null
|
|
249
|
+
this.schema = null
|
|
250
|
+
this._cancelWorkerRun = null
|
|
251
|
+
}
|
|
252
|
+
|
|
197
253
|
progress (i) {
|
|
198
254
|
const progressState = utils.getProgressState(i)
|
|
199
255
|
if (!progressState) {
|
|
@@ -234,6 +290,11 @@ export default class JSEE {
|
|
|
234
290
|
progress.style.transform = 'none'
|
|
235
291
|
progress.style.width = `${progressState.value}%`
|
|
236
292
|
}
|
|
293
|
+
|
|
294
|
+
// Overlay progress bar
|
|
295
|
+
if (this.overlay) {
|
|
296
|
+
this.overlay.setProgress(progressState)
|
|
297
|
+
}
|
|
237
298
|
}
|
|
238
299
|
|
|
239
300
|
async init () {
|
|
@@ -244,6 +305,12 @@ export default class JSEE {
|
|
|
244
305
|
this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
|
|
245
306
|
await this.initVue() // Inits: this.app, this.data
|
|
246
307
|
await this.initPipeline() // Inits: this.pipeline (function)
|
|
308
|
+
|
|
309
|
+
// Request notification permission if schema opts in
|
|
310
|
+
if (this.schema.notify && typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
311
|
+
Notification.requestPermission()
|
|
312
|
+
}
|
|
313
|
+
|
|
247
314
|
if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
|
|
248
315
|
// 1. If autorun is enabled in the schema, run the model immediately
|
|
249
316
|
// 2. Server-side inputs: If there are inputs with disabled and reactive flags
|
|
@@ -335,10 +402,10 @@ export default class JSEE {
|
|
|
335
402
|
}
|
|
336
403
|
})
|
|
337
404
|
|
|
338
|
-
// Check if model is empty
|
|
405
|
+
// Check if model is empty — allow identity pipeline (no model)
|
|
339
406
|
if (this.model.length === 0) {
|
|
340
|
-
|
|
341
|
-
|
|
407
|
+
log('No model defined, using identity pipeline')
|
|
408
|
+
return
|
|
342
409
|
}
|
|
343
410
|
|
|
344
411
|
// Put worker and imports inside model blocks
|
|
@@ -440,8 +507,12 @@ export default class JSEE {
|
|
|
440
507
|
// Relies on model.code
|
|
441
508
|
// So run after possible fetching
|
|
442
509
|
if (typeof this.schema.inputs === 'undefined') {
|
|
443
|
-
this.model
|
|
444
|
-
|
|
510
|
+
if (this.model.length > 0) {
|
|
511
|
+
this.model[0].container = 'args'
|
|
512
|
+
this.schema.inputs = getInputs(this.model[0])
|
|
513
|
+
} else {
|
|
514
|
+
this.schema.inputs = []
|
|
515
|
+
}
|
|
445
516
|
}
|
|
446
517
|
|
|
447
518
|
// Read URL params, e.g. ?input1=1&input2=2
|
|
@@ -482,14 +553,6 @@ export default class JSEE {
|
|
|
482
553
|
this.modelContainer = container.querySelector('#model')
|
|
483
554
|
// Init overlay
|
|
484
555
|
this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
|
|
485
|
-
// Stop button is shown only while a run is active
|
|
486
|
-
this.stopElement = document.createElement('button')
|
|
487
|
-
this.stopElement.innerHTML = 'Stop'
|
|
488
|
-
this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
|
|
489
|
-
this.stopElement.addEventListener('click', () => {
|
|
490
|
-
this.cancelCurrentRun()
|
|
491
|
-
})
|
|
492
|
-
this.overlay.element.appendChild(this.stopElement)
|
|
493
556
|
resolve()
|
|
494
557
|
}, log)
|
|
495
558
|
this.data = this.app.$data
|
|
@@ -576,11 +639,17 @@ export default class JSEE {
|
|
|
576
639
|
notyf.success('Pipeline initialized')
|
|
577
640
|
this.overlay.hide()
|
|
578
641
|
}
|
|
642
|
+
|
|
643
|
+
if (this.model.length === 0) {
|
|
644
|
+
log('Identity pipeline ready (no model)')
|
|
645
|
+
this.overlay.hide()
|
|
646
|
+
}
|
|
579
647
|
}
|
|
580
648
|
|
|
581
649
|
async initWorker (model) {
|
|
582
650
|
// Init worker
|
|
583
651
|
const worker = new Worker()
|
|
652
|
+
this._workers.push(worker)
|
|
584
653
|
|
|
585
654
|
// Init worker with the model
|
|
586
655
|
if (typeof model.code === 'function') {
|
|
@@ -605,6 +674,7 @@ export default class JSEE {
|
|
|
605
674
|
: utils.toWorkerSerializable(inputs)
|
|
606
675
|
|
|
607
676
|
const workerPromise = new Promise((resolve, reject) => {
|
|
677
|
+
this._rejectWorkerRun = reject
|
|
608
678
|
worker.onmessage = (e) => {
|
|
609
679
|
const res = e.data
|
|
610
680
|
if ((typeof res === 'object') && (res._status)) {
|
|
@@ -628,6 +698,12 @@ export default class JSEE {
|
|
|
628
698
|
reject(res._error)
|
|
629
699
|
break
|
|
630
700
|
}
|
|
701
|
+
} else if ((typeof res === 'object') && res._partial) {
|
|
702
|
+
// Partial/streaming result: update outputs without resolving the promise
|
|
703
|
+
log('Partial result from worker:', Object.keys(res))
|
|
704
|
+
const partial = { ...res }
|
|
705
|
+
delete partial._partial
|
|
706
|
+
this.output(partial)
|
|
631
707
|
} else {
|
|
632
708
|
log('Response from worker:', res)
|
|
633
709
|
this.progress(0)
|
|
@@ -641,7 +717,13 @@ export default class JSEE {
|
|
|
641
717
|
reject(e)
|
|
642
718
|
}
|
|
643
719
|
try {
|
|
644
|
-
|
|
720
|
+
// Transfer ArrayBuffers for zero-copy WASM passing
|
|
721
|
+
const transferables = utils.collectTransferables(payload)
|
|
722
|
+
if (transferables.length) {
|
|
723
|
+
worker.postMessage(payload, transferables)
|
|
724
|
+
} else {
|
|
725
|
+
worker.postMessage(payload)
|
|
726
|
+
}
|
|
645
727
|
} catch (error) {
|
|
646
728
|
const hasBinaryPayload = utils.containsBinaryPayload(payload)
|
|
647
729
|
if (hasBinaryPayload) {
|
|
@@ -688,7 +770,7 @@ export default class JSEE {
|
|
|
688
770
|
await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
|
|
689
771
|
const pyodide = await loadPyodide()
|
|
690
772
|
if (model.imports && Array.isArray(model.imports) && model.imports.length) {
|
|
691
|
-
await pyodide.loadPackage(model.imports.url)
|
|
773
|
+
await pyodide.loadPackage(model.imports.map(i => i.url))
|
|
692
774
|
} else {
|
|
693
775
|
await pyodide.loadPackagesFromImports(model.code)
|
|
694
776
|
}
|
|
@@ -747,8 +829,11 @@ export default class JSEE {
|
|
|
747
829
|
// Worker:
|
|
748
830
|
this.worker.postMessage(model)
|
|
749
831
|
} else {
|
|
750
|
-
// Main:
|
|
751
|
-
|
|
832
|
+
// Main: pass onChunk callback for SSE streaming
|
|
833
|
+
const onChunk = model.stream ? (chunk) => {
|
|
834
|
+
this.output(chunk)
|
|
835
|
+
} : undefined
|
|
836
|
+
return utils.getModelFuncAPI(model, log, onChunk)
|
|
752
837
|
}
|
|
753
838
|
}
|
|
754
839
|
|
|
@@ -780,10 +865,18 @@ export default class JSEE {
|
|
|
780
865
|
return
|
|
781
866
|
}
|
|
782
867
|
|
|
868
|
+
// Re-init model if workers were terminated by cancelCurrentRun
|
|
869
|
+
if (this._needsModelReinit) {
|
|
870
|
+
this._needsModelReinit = false
|
|
871
|
+
await this.initModel()
|
|
872
|
+
}
|
|
873
|
+
|
|
783
874
|
const schema = this.schema
|
|
784
875
|
const data = this.data
|
|
785
876
|
this.running = true
|
|
786
877
|
this.cancelled = false
|
|
878
|
+
let runSucceeded = false
|
|
879
|
+
let rejectCurrentRun = null
|
|
787
880
|
// Run token to detect stale results when worker.onmessage gets rebound
|
|
788
881
|
const runToken = this._runToken = {}
|
|
789
882
|
|
|
@@ -795,19 +888,54 @@ export default class JSEE {
|
|
|
795
888
|
// Skip buttons
|
|
796
889
|
if (input.name && !(input.type == 'action' || input.type == 'button')) {
|
|
797
890
|
inputValues[input.name] = getValue(input)
|
|
891
|
+
} else if (input.type === 'group' && !input.name && input.elements) {
|
|
892
|
+
// Unnamed groups (e.g. tabs layout): flatten children into top-level
|
|
893
|
+
input.elements.forEach(el => {
|
|
894
|
+
if (el.name) {
|
|
895
|
+
inputValues[el.name] = getValue(el)
|
|
896
|
+
}
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
// For folder inputs with select mode, filter to selected files
|
|
901
|
+
data.inputs.forEach(input => {
|
|
902
|
+
if (input.type === 'folder' && input.select && Array.isArray(inputValues[input.name])) {
|
|
903
|
+
const selected = inputValues[input.name].filter(f => f.selected)
|
|
904
|
+
inputValues[input.name] = selected
|
|
798
905
|
}
|
|
799
906
|
})
|
|
907
|
+
|
|
800
908
|
// Add caller to input values so we can change model behavior based on it
|
|
801
909
|
inputValues.caller = caller
|
|
802
910
|
|
|
911
|
+
// Chat mode: inject history and capture user message
|
|
912
|
+
const chatOutput = this.data.outputs
|
|
913
|
+
? this.data.outputs.find(o => o.type === 'chat')
|
|
914
|
+
: null
|
|
915
|
+
if (chatOutput) {
|
|
916
|
+
inputValues.history = chatOutput._messages || []
|
|
917
|
+
this._lastChatMessage = inputValues.message || ''
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Convert declared arrayBuffer inputs to typed arrays for WASM efficiency
|
|
921
|
+
inputValues = utils.wrapTypedArrayInputs(inputValues, schema.inputs)
|
|
922
|
+
|
|
803
923
|
log('Input values:', inputValues)
|
|
804
924
|
this.overlay.show()
|
|
805
|
-
if (this.
|
|
806
|
-
this.stopElement.style.display = 'inline-block'
|
|
807
|
-
}
|
|
925
|
+
if (this.data) this.data.running = true
|
|
808
926
|
|
|
809
927
|
// Run pipeline
|
|
810
|
-
const
|
|
928
|
+
const cancelPromise = new Promise((_, reject) => {
|
|
929
|
+
rejectCurrentRun = reject
|
|
930
|
+
this._rejectRun = reject
|
|
931
|
+
})
|
|
932
|
+
const pipelinePromise = Promise.resolve().then(() => this.pipeline(inputValues))
|
|
933
|
+
pipelinePromise.catch(err => {
|
|
934
|
+
if (this.cancelled || this._runToken !== runToken) {
|
|
935
|
+
log('Cancelled pipeline settled:', err)
|
|
936
|
+
}
|
|
937
|
+
})
|
|
938
|
+
const results = await Promise.race([pipelinePromise, cancelPromise])
|
|
811
939
|
|
|
812
940
|
// Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
|
|
813
941
|
if (this._runToken !== runToken) return
|
|
@@ -815,6 +943,18 @@ export default class JSEE {
|
|
|
815
943
|
// Output results
|
|
816
944
|
this.output(results)
|
|
817
945
|
|
|
946
|
+
// Chat mode: clear the text input for next message
|
|
947
|
+
if (chatOutput && this._lastChatMessage) {
|
|
948
|
+
data.inputs.forEach(input => {
|
|
949
|
+
if (input.name === 'message' || input.type === 'text') {
|
|
950
|
+
input.value = ''
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
this._lastChatMessage = null
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
runSucceeded = true
|
|
957
|
+
|
|
818
958
|
// Check if interval is defined
|
|
819
959
|
if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
|
|
820
960
|
log('Interval is defined:', schema.interval)
|
|
@@ -822,16 +962,29 @@ export default class JSEE {
|
|
|
822
962
|
await this.run(caller)
|
|
823
963
|
}
|
|
824
964
|
} catch (err) {
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
965
|
+
// Silently ignore cancellation errors from cancelCurrentRun()
|
|
966
|
+
if (this.cancelled && err && err.message === 'Cancelled') {
|
|
967
|
+
log('Run cancelled by user')
|
|
968
|
+
} else {
|
|
969
|
+
// Surface pipeline/worker errors so they don't silently swallow failures
|
|
970
|
+
log('Pipeline error:', err)
|
|
971
|
+
notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
|
|
972
|
+
}
|
|
828
973
|
} finally {
|
|
974
|
+
if (this._rejectRun === rejectCurrentRun) {
|
|
975
|
+
this._rejectRun = null
|
|
976
|
+
}
|
|
829
977
|
// Always clean up UI state so overlay and running flag don't get stuck
|
|
830
978
|
this.overlay.hide()
|
|
831
|
-
if (this.stopElement) {
|
|
832
|
-
this.stopElement.style.display = 'none'
|
|
833
|
-
}
|
|
834
979
|
this.running = false
|
|
980
|
+
if (this.data) this.data.running = false
|
|
981
|
+
|
|
982
|
+
// Notify user when tab is hidden and run succeeded
|
|
983
|
+
if (runSucceeded && schema.notify && document.hidden
|
|
984
|
+
&& typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
|
985
|
+
const title = this.model[0] && (this.model[0].title || this.model[0].name) || 'JSEE'
|
|
986
|
+
new Notification('Run complete', { body: title })
|
|
987
|
+
}
|
|
835
988
|
|
|
836
989
|
// Drain queued run if a manual click arrived while we were running
|
|
837
990
|
if (this._pendingRun) {
|
|
@@ -847,6 +1000,43 @@ export default class JSEE {
|
|
|
847
1000
|
await utils.delay(1)
|
|
848
1001
|
}
|
|
849
1002
|
|
|
1003
|
+
_mapResultsToOutputs (outputs, res) {
|
|
1004
|
+
outputs.forEach(output => {
|
|
1005
|
+
if (output.type === 'group' && output.elements) {
|
|
1006
|
+
this._mapResultsToOutputs(output.elements, res)
|
|
1007
|
+
} else {
|
|
1008
|
+
const r = res[output.name]
|
|
1009
|
+
|| res[utils.sanitizeName(output.name)]
|
|
1010
|
+
|| (output.alias && res[output.alias])
|
|
1011
|
+
if (typeof r !== 'undefined') {
|
|
1012
|
+
log(`Updating output: ${output.name} with data: ${typeof r}`)
|
|
1013
|
+
// Convert large base64 image data URLs to blob URLs for efficiency
|
|
1014
|
+
if (output.type === 'image' && typeof r === 'string'
|
|
1015
|
+
&& r.startsWith('data:') && r.length > 50000) {
|
|
1016
|
+
// Revoke previous blob URL
|
|
1017
|
+
if (output._objectUrl) {
|
|
1018
|
+
URL.revokeObjectURL(output._objectUrl)
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const [header, b64] = r.split(',')
|
|
1022
|
+
const mime = header.match(/data:([^;]+)/)[1]
|
|
1023
|
+
const binary = atob(b64)
|
|
1024
|
+
const bytes = new Uint8Array(binary.length)
|
|
1025
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
1026
|
+
const blob = new Blob([bytes], { type: mime })
|
|
1027
|
+
output._objectUrl = URL.createObjectURL(blob)
|
|
1028
|
+
output.value = output._objectUrl
|
|
1029
|
+
} catch (e) {
|
|
1030
|
+
output.value = r
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
output.value = r
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
|
|
850
1040
|
output (res) {
|
|
851
1041
|
// Edge case: no output field with reactivity is handled — undefined results exit early
|
|
852
1042
|
|
|
@@ -856,6 +1046,14 @@ export default class JSEE {
|
|
|
856
1046
|
|
|
857
1047
|
log('[Output] Got output results of type:', typeof res)
|
|
858
1048
|
|
|
1049
|
+
// Normalize primitive results to {result: value} for consistent rendering
|
|
1050
|
+
// regardless of execution mode (browser worker vs server POST)
|
|
1051
|
+
if ((typeof res !== 'object' || res === null) && !Array.isArray(res)) {
|
|
1052
|
+
if (!(this.schema.outputs && this.schema.outputs.length)) {
|
|
1053
|
+
res = { result: res }
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
859
1057
|
const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
|
|
860
1058
|
log('Input names:', inputNames)
|
|
861
1059
|
|
|
@@ -868,7 +1066,35 @@ export default class JSEE {
|
|
|
868
1066
|
delete res._progress
|
|
869
1067
|
log('Processing results as an object:', res)
|
|
870
1068
|
|
|
871
|
-
|
|
1069
|
+
// Chat mode: accumulate messages instead of replacing output
|
|
1070
|
+
if (this.data.outputs) {
|
|
1071
|
+
this.data.outputs.forEach(output => {
|
|
1072
|
+
if (output.type !== 'chat' || typeof res[output.name] === 'undefined') return
|
|
1073
|
+
if (!output._messages) output._messages = []
|
|
1074
|
+
if (this._lastChatMessage) {
|
|
1075
|
+
output._messages.push({ role: 'user', content: this._lastChatMessage })
|
|
1076
|
+
}
|
|
1077
|
+
const response = res[output.name]
|
|
1078
|
+
if (typeof response === 'string') {
|
|
1079
|
+
output._messages.push({ role: 'assistant', content: response })
|
|
1080
|
+
} else if (isObject(response) && response.content) {
|
|
1081
|
+
output._messages.push({ role: response.role || 'assistant', content: response.content })
|
|
1082
|
+
}
|
|
1083
|
+
delete res[output.name]
|
|
1084
|
+
})
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (this.model.length === 0) {
|
|
1088
|
+
// Identity mode: all keys become outputs, auto-detect types
|
|
1089
|
+
log('Identity mode: creating outputs from all result keys')
|
|
1090
|
+
this.data.outputs = Object.keys(res)
|
|
1091
|
+
.filter(key => key !== 'caller')
|
|
1092
|
+
.map(key => ({
|
|
1093
|
+
name: key,
|
|
1094
|
+
type: utils.inferOutputType(key, res[key]),
|
|
1095
|
+
value: res[key]
|
|
1096
|
+
}))
|
|
1097
|
+
} else if (Object.keys(res).every(key => inputNames.includes(key))) {
|
|
872
1098
|
// Update input fields from results
|
|
873
1099
|
// e.g. loading a csv file and updating list of target variables
|
|
874
1100
|
// This will be dynamically updated in the UI
|
|
@@ -889,16 +1115,7 @@ export default class JSEE {
|
|
|
889
1115
|
} else if (this.data.outputs && this.data.outputs.length) {
|
|
890
1116
|
// Update outputs from results
|
|
891
1117
|
log('Updating outputs from results with keys:', Object.keys(res))
|
|
892
|
-
this.data.outputs
|
|
893
|
-
// try output.name, sanitized output.name, output.alias
|
|
894
|
-
const r = res[output.name]
|
|
895
|
-
|| res[utils.sanitizeName(output.name)]
|
|
896
|
-
|| (output.alias && res[output.alias])
|
|
897
|
-
if (typeof r !== 'undefined') {
|
|
898
|
-
log(`Updating output: ${output.name} with data: ${typeof r}`)
|
|
899
|
-
output.value = r
|
|
900
|
-
}
|
|
901
|
-
})
|
|
1118
|
+
this._mapResultsToOutputs(this.data.outputs, res)
|
|
902
1119
|
} else if (!this.schema.render && !this.schema.view) {
|
|
903
1120
|
// There's no render or view defined in the schema, also:
|
|
904
1121
|
// No outputs defined, create outputs from results
|
|
@@ -909,8 +1126,7 @@ export default class JSEE {
|
|
|
909
1126
|
.map(key => {
|
|
910
1127
|
return {
|
|
911
1128
|
'name': key,
|
|
912
|
-
|
|
913
|
-
'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
|
|
1129
|
+
'type': utils.inferOutputType(key, res[key]),
|
|
914
1130
|
'value': res[key]
|
|
915
1131
|
}
|
|
916
1132
|
})
|
|
@@ -982,6 +1198,9 @@ export default class JSEE {
|
|
|
982
1198
|
console.error('Error removing GA script tags:', error.message)
|
|
983
1199
|
}
|
|
984
1200
|
|
|
1201
|
+
// Remove serve bar (bundled HTML is standalone, no server)
|
|
1202
|
+
try { clone.getElementById('jsee-serve-bar')?.remove() } catch (e) {}
|
|
1203
|
+
|
|
985
1204
|
console.log('Caching schema:', this.schema)
|
|
986
1205
|
storeInHiddenElement(this.schemaUrl, this.schema)
|
|
987
1206
|
|
package/src/overlay.js
CHANGED
|
@@ -2,8 +2,15 @@ class Overlay {
|
|
|
2
2
|
constructor (parent) {
|
|
3
3
|
this.element = document.createElement('div')
|
|
4
4
|
this.element.id = 'overlay'
|
|
5
|
-
this.element.innerHTML =
|
|
5
|
+
this.element.innerHTML = ''
|
|
6
6
|
parent.appendChild(this.element)
|
|
7
|
+
|
|
8
|
+
this.progressBar = document.createElement('div')
|
|
9
|
+
this.progressBar.style.cssText = 'display:none; width:200px; height:6px; background:#e0e0e0; border-radius:3px; overflow:hidden;'
|
|
10
|
+
this.progressFill = document.createElement('div')
|
|
11
|
+
this.progressFill.style.cssText = 'width:0%; height:100%; background:#00d1b2; border-radius:3px; transition:width 0.2s;'
|
|
12
|
+
this.progressBar.appendChild(this.progressFill)
|
|
13
|
+
this.element.appendChild(this.progressBar)
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
show () {
|
|
@@ -12,6 +19,24 @@ class Overlay {
|
|
|
12
19
|
|
|
13
20
|
hide () {
|
|
14
21
|
this.element.style.display = 'none'
|
|
22
|
+
this.setProgress(null)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setProgress (state) {
|
|
26
|
+
if (!state) {
|
|
27
|
+
this.progressBar.style.display = 'none'
|
|
28
|
+
this.progressFill.style.width = '0%'
|
|
29
|
+
this.progressFill.style.animation = 'none'
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
this.progressBar.style.display = 'block'
|
|
33
|
+
if (state.mode === 'indeterminate') {
|
|
34
|
+
this.progressFill.style.width = '30%'
|
|
35
|
+
this.progressFill.style.animation = 'jsee-progress-indeterminate 1.2s ease-in-out infinite'
|
|
36
|
+
} else {
|
|
37
|
+
this.progressFill.style.animation = 'none'
|
|
38
|
+
this.progressFill.style.width = state.value + '%'
|
|
39
|
+
}
|
|
15
40
|
}
|
|
16
41
|
}
|
|
17
42
|
|