@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -55
  4. package/dist/2b3e1faf89f94a483539.png +0 -0
  5. package/dist/416d91365b44e4b4f477.png +0 -0
  6. package/dist/8f2c4d11474275fbc161.png +0 -0
  7. package/dist/jsee.core.js +1 -0
  8. package/dist/jsee.full.js +1 -0
  9. package/dist/jsee.runtime.js +2 -1
  10. package/package.json +84 -18
  11. package/src/app.js +130 -33
  12. package/src/cli.js +474 -64
  13. package/src/extended-imports.js +11 -0
  14. package/src/main.js +264 -45
  15. package/src/overlay.js +26 -1
  16. package/src/utils.js +390 -12
  17. package/templates/common-inputs.js +88 -0
  18. package/templates/common-outputs.js +340 -4
  19. package/templates/minimal-app.vue +367 -0
  20. package/templates/minimal-input.vue +573 -0
  21. package/templates/minimal-output.vue +426 -0
  22. package/templates/virtual-table.vue +194 -0
  23. package/.claude/settings.local.json +0 -13
  24. package/.eslintrc.js +0 -38
  25. package/AGENTS.md +0 -65
  26. package/CLAUDE.md +0 -5
  27. package/CNAME +0 -1
  28. package/_config.yml +0 -26
  29. package/dist/jsee.js +0 -1
  30. package/jest-puppeteer.config.js +0 -14
  31. package/jest.config.js +0 -8
  32. package/jest.unit.config.js +0 -8
  33. package/load/index.html +0 -52
  34. package/templates/bulma-app.vue +0 -242
  35. package/templates/bulma-input.vue +0 -125
  36. package/templates/bulma-output.vue +0 -101
  37. package/test/class.html +0 -22
  38. package/test/code.html +0 -25
  39. package/test/codew.html +0 -25
  40. package/test/example-class.js +0 -8
  41. package/test/example-sum.js +0 -3
  42. package/test/fixtures/lodash-like.js +0 -15
  43. package/test/fixtures/upload-sample.csv +0 -3
  44. package/test/importw.html +0 -28
  45. package/test/minimal.html +0 -14
  46. package/test/minimal1.html +0 -13
  47. package/test/minimal2.html +0 -15
  48. package/test/minimal3.html +0 -10
  49. package/test/minimal4.html +0 -22
  50. package/test/pipeline.html +0 -52
  51. package/test/python.html +0 -41
  52. package/test/string.html +0 -26
  53. package/test/stringw.html +0 -29
  54. package/test/sum.schema.json +0 -17
  55. package/test/sumw.schema.json +0 -15
  56. package/test/test-basic.test.js +0 -603
  57. package/test/test-python.test.js +0 -23
  58. package/test/unit/cli-fetch.test.js +0 -229
  59. package/test/unit/utils.test.js +0 -888
  60. 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
- notyf.error('Model is in a wrong format')
341
- throw new Error(`Model is in a wrong format: ${this.schema.model}`)
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[0].container = 'args'
444
- this.schema.inputs = getInputs(this.model[0])
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
- worker.postMessage(payload)
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
- return utils.getModelFuncAPI(model, log)
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.stopElement) {
806
- this.stopElement.style.display = 'inline-block'
807
- }
925
+ if (this.data) this.data.running = true
808
926
 
809
927
  // Run pipeline
810
- const results = await this.pipeline(inputValues)
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
- // Surface pipeline/worker errors so they don't silently swallow failures
826
- log('Pipeline error:', err)
827
- notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
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
- if (Object.keys(res).every(key => inputNames.includes(key))) {
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.forEach((output, i) => {
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
- // typeof returns 'object' for arrays; distinguish them for proper rendering
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