@jseeio/jsee 0.4.2 → 0.8.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/CHANGELOG.md +96 -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 +127 -32
- package/src/browser-bundle-node.js +9 -0
- package/src/cli.js +479 -67
- package/src/extended-imports.js +11 -0
- package/src/main.js +232 -44
- package/src/overlay.js +26 -1
- package/src/utils.js +386 -16
- 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 -15
- 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/dump.sh +0 -23
- package/jest-puppeteer.config.js +0 -14
- package/jest.config.js +0 -8
- package/jest.unit.config.js +0 -8
- package/jsee.dump.txt +0 -5459
- 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/arrow-main.html +0 -18
- package/test/arrow-worker.html +0 -18
- 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/runtime-arrow.html +0 -18
- 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 -630
- package/test/test-python.test.js +0 -23
- package/test/unit/cli-fetch.test.js +0 -229
- package/test/unit/utils.test.js +0 -908
- 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,7 +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
|
|
155
157
|
this._workers = []
|
|
158
|
+
this._pendingRun = null
|
|
159
|
+
this._runToken = null
|
|
160
|
+
this._needsModelReinit = false
|
|
156
161
|
|
|
157
162
|
// Check if schema is provided
|
|
158
163
|
if (typeof this.schema === 'undefined') {
|
|
@@ -185,10 +190,31 @@ export default class JSEE {
|
|
|
185
190
|
|
|
186
191
|
cancelCurrentRun () {
|
|
187
192
|
log('Stopping current run')
|
|
193
|
+
const cancelError = new Error('Cancelled')
|
|
188
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
|
+
}
|
|
189
201
|
if (typeof this._cancelWorkerRun === 'function') {
|
|
190
202
|
this._cancelWorkerRun()
|
|
191
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
|
|
192
218
|
}
|
|
193
219
|
|
|
194
220
|
isCancelled () {
|
|
@@ -264,6 +290,11 @@ export default class JSEE {
|
|
|
264
290
|
progress.style.transform = 'none'
|
|
265
291
|
progress.style.width = `${progressState.value}%`
|
|
266
292
|
}
|
|
293
|
+
|
|
294
|
+
// Overlay progress bar
|
|
295
|
+
if (this.overlay) {
|
|
296
|
+
this.overlay.setProgress(progressState)
|
|
297
|
+
}
|
|
267
298
|
}
|
|
268
299
|
|
|
269
300
|
async init () {
|
|
@@ -274,6 +305,12 @@ export default class JSEE {
|
|
|
274
305
|
this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
|
|
275
306
|
await this.initVue() // Inits: this.app, this.data
|
|
276
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
|
+
|
|
277
314
|
if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
|
|
278
315
|
// 1. If autorun is enabled in the schema, run the model immediately
|
|
279
316
|
// 2. Server-side inputs: If there are inputs with disabled and reactive flags
|
|
@@ -365,10 +402,10 @@ export default class JSEE {
|
|
|
365
402
|
}
|
|
366
403
|
})
|
|
367
404
|
|
|
368
|
-
// Check if model is empty
|
|
405
|
+
// Check if model is empty — allow identity pipeline (no model)
|
|
369
406
|
if (this.model.length === 0) {
|
|
370
|
-
|
|
371
|
-
|
|
407
|
+
log('No model defined, using identity pipeline')
|
|
408
|
+
return
|
|
372
409
|
}
|
|
373
410
|
|
|
374
411
|
// Put worker and imports inside model blocks
|
|
@@ -470,8 +507,12 @@ export default class JSEE {
|
|
|
470
507
|
// Relies on model.code
|
|
471
508
|
// So run after possible fetching
|
|
472
509
|
if (typeof this.schema.inputs === 'undefined') {
|
|
473
|
-
this.model
|
|
474
|
-
|
|
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
|
+
}
|
|
475
516
|
}
|
|
476
517
|
|
|
477
518
|
// Read URL params, e.g. ?input1=1&input2=2
|
|
@@ -512,14 +553,6 @@ export default class JSEE {
|
|
|
512
553
|
this.modelContainer = container.querySelector('#model')
|
|
513
554
|
// Init overlay
|
|
514
555
|
this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
|
|
515
|
-
// Stop button is shown only while a run is active
|
|
516
|
-
this.stopElement = document.createElement('button')
|
|
517
|
-
this.stopElement.innerHTML = 'Stop'
|
|
518
|
-
this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
|
|
519
|
-
this.stopElement.addEventListener('click', () => {
|
|
520
|
-
this.cancelCurrentRun()
|
|
521
|
-
})
|
|
522
|
-
this.overlay.element.appendChild(this.stopElement)
|
|
523
556
|
resolve()
|
|
524
557
|
}, log)
|
|
525
558
|
this.data = this.app.$data
|
|
@@ -606,6 +639,11 @@ export default class JSEE {
|
|
|
606
639
|
notyf.success('Pipeline initialized')
|
|
607
640
|
this.overlay.hide()
|
|
608
641
|
}
|
|
642
|
+
|
|
643
|
+
if (this.model.length === 0) {
|
|
644
|
+
log('Identity pipeline ready (no model)')
|
|
645
|
+
this.overlay.hide()
|
|
646
|
+
}
|
|
609
647
|
}
|
|
610
648
|
|
|
611
649
|
async initWorker (model) {
|
|
@@ -636,6 +674,7 @@ export default class JSEE {
|
|
|
636
674
|
: utils.toWorkerSerializable(inputs)
|
|
637
675
|
|
|
638
676
|
const workerPromise = new Promise((resolve, reject) => {
|
|
677
|
+
this._rejectWorkerRun = reject
|
|
639
678
|
worker.onmessage = (e) => {
|
|
640
679
|
const res = e.data
|
|
641
680
|
if ((typeof res === 'object') && (res._status)) {
|
|
@@ -659,6 +698,12 @@ export default class JSEE {
|
|
|
659
698
|
reject(res._error)
|
|
660
699
|
break
|
|
661
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)
|
|
662
707
|
} else {
|
|
663
708
|
log('Response from worker:', res)
|
|
664
709
|
this.progress(0)
|
|
@@ -672,7 +717,13 @@ export default class JSEE {
|
|
|
672
717
|
reject(e)
|
|
673
718
|
}
|
|
674
719
|
try {
|
|
675
|
-
|
|
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
|
+
}
|
|
676
727
|
} catch (error) {
|
|
677
728
|
const hasBinaryPayload = utils.containsBinaryPayload(payload)
|
|
678
729
|
if (hasBinaryPayload) {
|
|
@@ -719,7 +770,7 @@ export default class JSEE {
|
|
|
719
770
|
await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
|
|
720
771
|
const pyodide = await loadPyodide()
|
|
721
772
|
if (model.imports && Array.isArray(model.imports) && model.imports.length) {
|
|
722
|
-
await pyodide.loadPackage(model.imports.url)
|
|
773
|
+
await pyodide.loadPackage(model.imports.map(i => i.url))
|
|
723
774
|
} else {
|
|
724
775
|
await pyodide.loadPackagesFromImports(model.code)
|
|
725
776
|
}
|
|
@@ -778,8 +829,11 @@ export default class JSEE {
|
|
|
778
829
|
// Worker:
|
|
779
830
|
this.worker.postMessage(model)
|
|
780
831
|
} else {
|
|
781
|
-
// Main:
|
|
782
|
-
|
|
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)
|
|
783
837
|
}
|
|
784
838
|
}
|
|
785
839
|
|
|
@@ -811,10 +865,18 @@ export default class JSEE {
|
|
|
811
865
|
return
|
|
812
866
|
}
|
|
813
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
|
+
|
|
814
874
|
const schema = this.schema
|
|
815
875
|
const data = this.data
|
|
816
876
|
this.running = true
|
|
817
877
|
this.cancelled = false
|
|
878
|
+
let runSucceeded = false
|
|
879
|
+
let rejectCurrentRun = null
|
|
818
880
|
// Run token to detect stale results when worker.onmessage gets rebound
|
|
819
881
|
const runToken = this._runToken = {}
|
|
820
882
|
|
|
@@ -826,19 +888,54 @@ export default class JSEE {
|
|
|
826
888
|
// Skip buttons
|
|
827
889
|
if (input.name && !(input.type == 'action' || input.type == 'button')) {
|
|
828
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
|
+
})
|
|
829
898
|
}
|
|
830
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
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
|
|
831
908
|
// Add caller to input values so we can change model behavior based on it
|
|
832
909
|
inputValues.caller = caller
|
|
833
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
|
+
|
|
834
923
|
log('Input values:', inputValues)
|
|
835
924
|
this.overlay.show()
|
|
836
|
-
if (this.
|
|
837
|
-
this.stopElement.style.display = 'inline-block'
|
|
838
|
-
}
|
|
925
|
+
if (this.data) this.data.running = true
|
|
839
926
|
|
|
840
927
|
// Run pipeline
|
|
841
|
-
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])
|
|
842
939
|
|
|
843
940
|
// Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
|
|
844
941
|
if (this._runToken !== runToken) return
|
|
@@ -846,6 +943,18 @@ export default class JSEE {
|
|
|
846
943
|
// Output results
|
|
847
944
|
this.output(results)
|
|
848
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
|
+
|
|
849
958
|
// Check if interval is defined
|
|
850
959
|
if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
|
|
851
960
|
log('Interval is defined:', schema.interval)
|
|
@@ -853,16 +962,29 @@ export default class JSEE {
|
|
|
853
962
|
await this.run(caller)
|
|
854
963
|
}
|
|
855
964
|
} catch (err) {
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
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
|
+
}
|
|
859
973
|
} finally {
|
|
974
|
+
if (this._rejectRun === rejectCurrentRun) {
|
|
975
|
+
this._rejectRun = null
|
|
976
|
+
}
|
|
860
977
|
// Always clean up UI state so overlay and running flag don't get stuck
|
|
861
978
|
this.overlay.hide()
|
|
862
|
-
if (this.stopElement) {
|
|
863
|
-
this.stopElement.style.display = 'none'
|
|
864
|
-
}
|
|
865
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
|
+
}
|
|
866
988
|
|
|
867
989
|
// Drain queued run if a manual click arrived while we were running
|
|
868
990
|
if (this._pendingRun) {
|
|
@@ -878,6 +1000,43 @@ export default class JSEE {
|
|
|
878
1000
|
await utils.delay(1)
|
|
879
1001
|
}
|
|
880
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
|
+
|
|
881
1040
|
output (res) {
|
|
882
1041
|
// Edge case: no output field with reactivity is handled — undefined results exit early
|
|
883
1042
|
|
|
@@ -887,6 +1046,14 @@ export default class JSEE {
|
|
|
887
1046
|
|
|
888
1047
|
log('[Output] Got output results of type:', typeof res)
|
|
889
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
|
+
|
|
890
1057
|
const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
|
|
891
1058
|
log('Input names:', inputNames)
|
|
892
1059
|
|
|
@@ -899,7 +1066,35 @@ export default class JSEE {
|
|
|
899
1066
|
delete res._progress
|
|
900
1067
|
log('Processing results as an object:', res)
|
|
901
1068
|
|
|
902
|
-
|
|
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))) {
|
|
903
1098
|
// Update input fields from results
|
|
904
1099
|
// e.g. loading a csv file and updating list of target variables
|
|
905
1100
|
// This will be dynamically updated in the UI
|
|
@@ -920,16 +1115,7 @@ export default class JSEE {
|
|
|
920
1115
|
} else if (this.data.outputs && this.data.outputs.length) {
|
|
921
1116
|
// Update outputs from results
|
|
922
1117
|
log('Updating outputs from results with keys:', Object.keys(res))
|
|
923
|
-
this.data.outputs
|
|
924
|
-
// try output.name, sanitized output.name, output.alias
|
|
925
|
-
const r = res[output.name]
|
|
926
|
-
|| res[utils.sanitizeName(output.name)]
|
|
927
|
-
|| (output.alias && res[output.alias])
|
|
928
|
-
if (typeof r !== 'undefined') {
|
|
929
|
-
log(`Updating output: ${output.name} with data: ${typeof r}`)
|
|
930
|
-
output.value = r
|
|
931
|
-
}
|
|
932
|
-
})
|
|
1118
|
+
this._mapResultsToOutputs(this.data.outputs, res)
|
|
933
1119
|
} else if (!this.schema.render && !this.schema.view) {
|
|
934
1120
|
// There's no render or view defined in the schema, also:
|
|
935
1121
|
// No outputs defined, create outputs from results
|
|
@@ -940,8 +1126,7 @@ export default class JSEE {
|
|
|
940
1126
|
.map(key => {
|
|
941
1127
|
return {
|
|
942
1128
|
'name': key,
|
|
943
|
-
|
|
944
|
-
'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
|
|
1129
|
+
'type': utils.inferOutputType(key, res[key]),
|
|
945
1130
|
'value': res[key]
|
|
946
1131
|
}
|
|
947
1132
|
})
|
|
@@ -1013,6 +1198,9 @@ export default class JSEE {
|
|
|
1013
1198
|
console.error('Error removing GA script tags:', error.message)
|
|
1014
1199
|
}
|
|
1015
1200
|
|
|
1201
|
+
// Remove serve bar (bundled HTML is standalone, no server)
|
|
1202
|
+
try { clone.getElementById('jsee-serve-bar')?.remove() } catch (e) {}
|
|
1203
|
+
|
|
1016
1204
|
console.log('Caching schema:', this.schema)
|
|
1017
1205
|
storeInHiddenElement(this.schemaUrl, this.schema)
|
|
1018
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
|
|