@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
@@ -1,9 +1,17 @@
1
1
  import { saveAs } from 'file-saver'
2
2
  import domtoimage from 'dom-to-image'
3
+ import MarkdownIt from 'markdown-it'
3
4
 
4
- const { sanitizeName } = require('../src/utils.js')
5
+ const { sanitizeName, columnsToRows } = require('../src/utils.js')
6
+
7
+ const mdConverter = new MarkdownIt({
8
+ html: true,
9
+ linkify: true,
10
+ typographer: false
11
+ })
5
12
 
6
13
  const Blob = window['Blob']
14
+ const UrlApi = window['URL'] || window['webkitURL']
7
15
 
8
16
  function stringify (v) {
9
17
  return typeof v === 'string'
@@ -18,19 +26,30 @@ const component = {
18
26
  return {
19
27
  outputName: 'output',
20
28
  isFullScreen: false,
29
+ activeOutputTab: 0,
30
+ lightboxSrc: null,
31
+ _threeScene: null,
32
+ _threeRenderer: null,
33
+ _threeAnimId: null,
34
+ _leafletMap: null,
35
+ _pdfObjectUrl: null,
21
36
  }
22
37
  },
23
38
  mounted() {
24
- this.outputName = this.output.alias
39
+ this.outputName = this.output.alias
25
40
  ? this.output.alias
26
41
  : this.output.name
27
- ? sanitizeName(this.output.name)
42
+ ? sanitizeName(this.output.name)
28
43
  : 'output_' + Math.floor(Math.random() * 1000000)
29
44
  this.executeRenderFunction()
30
45
  document.addEventListener('fullscreenchange', this.onFullScreenChange)
31
46
  },
32
47
  beforeUnmount() {
33
48
  document.removeEventListener('fullscreenchange', this.onFullScreenChange)
49
+ if (this._threeAnimId) cancelAnimationFrame(this._threeAnimId)
50
+ if (this._threeRenderer) this._threeRenderer.dispose()
51
+ if (this._leafletMap) this._leafletMap.remove()
52
+ this.revokePdfObjectUrl()
34
53
  },
35
54
  // updated() {
36
55
  // this.executeRenderFunction()
@@ -40,6 +59,20 @@ const component = {
40
59
  if (newValue !== oldValue) {
41
60
  this.$nextTick(() => {
42
61
  this.executeRenderFunction()
62
+ if (this.output.type === 'chart') this.renderChart()
63
+ if (this.output.type === '3d') this.render3D()
64
+ if (this.output.type === 'map') this.renderMap()
65
+ if (this.output.type === 'pdf') this.renderPDF()
66
+ })
67
+ }
68
+ },
69
+ 'output._messages': {
70
+ deep: true,
71
+ handler () {
72
+ this.$nextTick(() => {
73
+ if (this.$refs.chatMessages) {
74
+ this.$refs.chatMessages.scrollTop = this.$refs.chatMessages.scrollHeight
75
+ }
43
76
  })
44
77
  }
45
78
  }
@@ -47,6 +80,31 @@ const component = {
47
80
  computed: {
48
81
  isRenderFunction() {
49
82
  return typeof this.output.value === 'function'
83
+ },
84
+ hasPlot() {
85
+ return typeof window !== 'undefined' && !!window.Plot
86
+ },
87
+ hasThree() {
88
+ return typeof window !== 'undefined' && !!window.THREE
89
+ },
90
+ hasLeaflet() {
91
+ return typeof window !== 'undefined' && !!window.L
92
+ },
93
+ viewerMedia () {
94
+ const url = this.output && this.output.value
95
+ if (typeof url !== 'string' || !url) return null
96
+ let filePath = url
97
+ try {
98
+ const parsed = new URL(url, 'http://x')
99
+ if (parsed.searchParams.has('path')) filePath = parsed.searchParams.get('path')
100
+ } catch (e) {}
101
+ const dot = filePath.lastIndexOf('.')
102
+ if (dot < 0) return 'iframe'
103
+ const ext = filePath.slice(dot).toLowerCase()
104
+ if (/^\.(png|jpe?g|gif|svg|webp|bmp|ico)$/.test(ext)) return 'image'
105
+ if (/^\.(mp3|wav|ogg|flac|aac)$/.test(ext)) return 'audio'
106
+ if (/^\.(mp4|webm|mov|avi)$/.test(ext)) return 'video'
107
+ return 'iframe'
50
108
  }
51
109
  },
52
110
  methods: {
@@ -79,6 +137,21 @@ const component = {
79
137
  onFullScreenChange() {
80
138
  this.isFullScreen = !!document.fullscreenElement
81
139
  },
140
+ tableToDelimited (delim) {
141
+ const d = this.output.value
142
+ if (!d || !d.columns) return ''
143
+ const esc = (v) => {
144
+ const s = String(v == null ? '' : v)
145
+ return s.indexOf(delim) >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0
146
+ ? '"' + s.replace(/"/g, '""') + '"'
147
+ : s
148
+ }
149
+ const lines = [d.columns.map(esc).join(delim)]
150
+ for (const row of d.rows) {
151
+ lines.push(row.map(esc).join(delim))
152
+ }
153
+ return lines.join('\n')
154
+ },
82
155
  save () {
83
156
  // Prepare filename
84
157
  let filename
@@ -94,6 +167,18 @@ const component = {
94
167
  case 'svg':
95
168
  extension = 'svg'
96
169
  break
170
+ case 'table':
171
+ extension = 'csv'
172
+ break
173
+ case 'markdown':
174
+ extension = 'md'
175
+ break
176
+ case 'image':
177
+ extension = 'png'
178
+ break
179
+ case 'chart':
180
+ extension = 'svg'
181
+ break
97
182
  default:
98
183
  extension = 'txt'
99
184
  }
@@ -101,13 +186,34 @@ const component = {
101
186
  }
102
187
 
103
188
  // Prepare blob
189
+ if (this.output.type === 'chart' && this.$refs.chartContainer) {
190
+ const svg = this.$refs.chartContainer.querySelector('svg')
191
+ if (svg) {
192
+ let blob = new Blob([svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' })
193
+ saveAs(blob, filename)
194
+ }
195
+ return
196
+ }
197
+ if (this.output.type === 'image') {
198
+ fetch(this.output.value)
199
+ .then(r => r.blob())
200
+ .then(blob => saveAs(blob, filename))
201
+ .catch(() => {
202
+ let blob = new Blob([this.output.value], {type: 'text/plain;charset=utf-8'})
203
+ saveAs(blob, filename)
204
+ })
205
+ return
206
+ }
104
207
  if (this.output.type === 'function') {
105
208
  domtoimage.toBlob(this.$refs.customContainer)
106
209
  .then(blob => {
107
210
  saveAs(blob, filename)
108
211
  })
212
+ return
109
213
  }
110
- let value = stringify(this.output.value)
214
+ let value = this.output.type === 'table'
215
+ ? this.tableToDelimited(',')
216
+ : stringify(this.output.value)
111
217
  let blob = new Blob([value], {type: 'text/plain;charset=utf-8'})
112
218
  saveAs(blob, filename)
113
219
  },
@@ -130,12 +236,242 @@ const component = {
130
236
  console.error('Failed to generate image blob: ', err);
131
237
  this.$emit('notification', 'Failed to generate image');
132
238
  });
239
+ } else if (this.output.type === 'image') {
240
+ fetch(this.output.value)
241
+ .then(r => r.blob())
242
+ .then(blob => {
243
+ const item = new ClipboardItem({ [blob.type]: blob })
244
+ navigator.clipboard.write([item])
245
+ .then(() => this.$emit('notification', 'Image copied to clipboard'))
246
+ .catch(() => this.$emit('notification', 'Failed to copy image'))
247
+ })
248
+ .catch(() => {
249
+ navigator.clipboard.writeText(this.output.value)
250
+ this.$emit('notification', 'Copied image URL')
251
+ })
252
+ } else if (this.output.type === 'table') {
253
+ let value = this.tableToDelimited('\t')
254
+ navigator.clipboard.writeText(value)
255
+ this.$emit('notification', 'Copied as TSV')
133
256
  } else {
134
257
  let value = stringify(this.output.value)
135
258
  navigator.clipboard.writeText(value)
136
259
  this.$emit('notification', 'Copied')
137
260
  }
138
261
  },
262
+ downloadFile () {
263
+ let filename = this.output.filename || this.output.name || 'output'
264
+ let value = this.output.value
265
+ if (typeof value === 'string' && value.startsWith('data:')) {
266
+ fetch(value)
267
+ .then(r => r.blob())
268
+ .then(blob => saveAs(blob, filename))
269
+ .catch(() => {
270
+ let blob = new Blob([value], { type: 'application/octet-stream' })
271
+ saveAs(blob, filename)
272
+ })
273
+ } else {
274
+ let content = typeof value === 'string' ? value : JSON.stringify(value)
275
+ let blob = new Blob([content], { type: 'application/octet-stream' })
276
+ saveAs(blob, filename)
277
+ }
278
+ },
279
+ renderMarkdown (text) {
280
+ if (typeof text !== 'string') return ''
281
+ return mdConverter.render(text)
282
+ },
283
+ renderChart() {
284
+ if (!this.hasPlot || !this.$refs.chartContainer) return
285
+ const container = this.$refs.chartContainer
286
+ const data = this.output.value
287
+ if (!data) return
288
+ container.innerHTML = ''
289
+ try {
290
+ let plotConfig
291
+ if (data && data.marks) {
292
+ // Full Plot config passed directly
293
+ plotConfig = data
294
+ } else {
295
+ // Build config from schema props + data
296
+ let rows = Array.isArray(data) ? data : columnsToRows(data)
297
+ if (!Array.isArray(rows)) return
298
+ const mark = this.output.mark || 'dot'
299
+ const x = this.output.x || (rows[0] && Object.keys(rows[0])[0])
300
+ const y = this.output.y || (rows[0] && Object.keys(rows[0])[1])
301
+ const color = this.output.color
302
+ const markOpts = { x, y }
303
+ if (color) markOpts.fill = color
304
+ const Plot = window.Plot
305
+ const markFn = Plot[mark] || Plot.dot
306
+ plotConfig = {
307
+ marks: [markFn(rows, markOpts)],
308
+ width: this.output.width || 640,
309
+ height: this.output.height || 400
310
+ }
311
+ }
312
+ const svg = window.Plot.plot(plotConfig)
313
+ container.appendChild(svg)
314
+ } catch (e) {
315
+ container.textContent = 'Chart error: ' + e.message
316
+ }
317
+ },
318
+ render3D() {
319
+ if (!this.hasThree || !this.$refs.threeDContainer) return
320
+ const container = this.$refs.threeDContainer
321
+ const data = this.output.value
322
+ if (!data) return
323
+ // Dispose previous scene
324
+ if (this._threeAnimId) cancelAnimationFrame(this._threeAnimId)
325
+ if (this._threeRenderer) {
326
+ this._threeRenderer.dispose()
327
+ if (this._threeRenderer.domElement && this._threeRenderer.domElement.parentNode) {
328
+ this._threeRenderer.domElement.parentNode.removeChild(this._threeRenderer.domElement)
329
+ }
330
+ }
331
+ const THREE = window.THREE
332
+ const width = container.clientWidth || 640
333
+ const height = this.output.height || 400
334
+ const scene = new THREE.Scene()
335
+ scene.background = new THREE.Color(0xf0f0f0)
336
+ const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000)
337
+ camera.position.set(0, 1, 3)
338
+ const renderer = new THREE.WebGLRenderer({ antialias: true })
339
+ renderer.setSize(width, height)
340
+ // Remove only previous canvas, keep missing-message divs
341
+ const oldCanvas = container.querySelector('canvas')
342
+ if (oldCanvas) oldCanvas.remove()
343
+ container.appendChild(renderer.domElement)
344
+ scene.add(new THREE.AmbientLight(0xcccccc, 0.6))
345
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8)
346
+ dirLight.position.set(1, 2, 3)
347
+ scene.add(dirLight)
348
+ this._threeScene = scene
349
+ this._threeRenderer = renderer
350
+ const animate = () => {
351
+ this._threeAnimId = requestAnimationFrame(animate)
352
+ renderer.render(scene, camera)
353
+ }
354
+ if (typeof data === 'object' && data.vertices) {
355
+ // Programmatic geometry
356
+ const geom = new THREE.BufferGeometry()
357
+ geom.setAttribute('position', new THREE.Float32BufferAttribute(data.vertices, 3))
358
+ if (data.faces) geom.setIndex(data.faces)
359
+ geom.computeVertexNormals()
360
+ const mesh = new THREE.Mesh(geom, new THREE.MeshStandardMaterial({ color: 0x00d1b2 }))
361
+ scene.add(mesh)
362
+ animate()
363
+ } else if (typeof data === 'string') {
364
+ // URL to GLTF/GLB — needs GLTFLoader loaded via imports
365
+ if (THREE.GLTFLoader) {
366
+ const loader = new THREE.GLTFLoader()
367
+ loader.load(data, (gltf) => {
368
+ scene.add(gltf.scene)
369
+ // Auto-fit camera
370
+ const box = new THREE.Box3().setFromObject(gltf.scene)
371
+ const center = box.getCenter(new THREE.Vector3())
372
+ const size = box.getSize(new THREE.Vector3()).length()
373
+ camera.position.copy(center).add(new THREE.Vector3(0, size * 0.5, size))
374
+ camera.lookAt(center)
375
+ animate()
376
+ })
377
+ } else {
378
+ container.textContent = '3D URL loading requires GLTFLoader in imports'
379
+ }
380
+ }
381
+ },
382
+ renderMap() {
383
+ if (!this.hasLeaflet || !this.$refs.mapContainer) return
384
+ const container = this.$refs.mapContainer
385
+ const data = this.output.value
386
+ if (!data) return
387
+ const L = window.L
388
+ // Destroy previous map
389
+ if (this._leafletMap) {
390
+ this._leafletMap.remove()
391
+ this._leafletMap = null
392
+ }
393
+ // Remove any existing map container content except missing message
394
+ const existingMap = container.querySelector('.leaflet-container')
395
+ if (existingMap) existingMap.remove()
396
+ const mapDiv = document.createElement('div')
397
+ mapDiv.style.width = '100%'
398
+ mapDiv.style.height = '100%'
399
+ container.appendChild(mapDiv)
400
+ const zoom = this.output.zoom || data.zoom || 13
401
+ const tiles = this.output.tiles || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
402
+ const map = L.map(mapDiv)
403
+ L.tileLayer(tiles, { attribution: '&copy; OpenStreetMap' }).addTo(map)
404
+ this._leafletMap = map
405
+ // GeoJSON
406
+ if (data.type === 'FeatureCollection' || data.type === 'Feature') {
407
+ const layer = L.geoJSON(data).addTo(map)
408
+ map.fitBounds(layer.getBounds())
409
+ return
410
+ }
411
+ // Markers from array or object
412
+ const markers = Array.isArray(data) ? data : (data.markers || [])
413
+ const center = this.output.center || data.center
414
+ const bounds = []
415
+ markers.forEach(m => {
416
+ const latlng = [m.lat, m.lng]
417
+ const marker = L.marker(latlng).addTo(map)
418
+ if (m.popup) marker.bindPopup(m.popup)
419
+ bounds.push(latlng)
420
+ })
421
+ if (center) {
422
+ map.setView(center, zoom)
423
+ } else if (bounds.length) {
424
+ map.fitBounds(bounds)
425
+ } else {
426
+ map.setView([0, 0], 2)
427
+ }
428
+ },
429
+ revokePdfObjectUrl() {
430
+ if (this._pdfObjectUrl && UrlApi) {
431
+ UrlApi.revokeObjectURL(this._pdfObjectUrl)
432
+ this._pdfObjectUrl = null
433
+ }
434
+ },
435
+ pdfDataToUrl(data) {
436
+ if (typeof data === 'string') return data
437
+ if (!Blob || !UrlApi) return null
438
+ if (data instanceof Blob) {
439
+ this.revokePdfObjectUrl()
440
+ this._pdfObjectUrl = UrlApi.createObjectURL(data)
441
+ return this._pdfObjectUrl
442
+ }
443
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
444
+ this.revokePdfObjectUrl()
445
+ const bytes = data instanceof ArrayBuffer
446
+ ? new Uint8Array(data)
447
+ : new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength)
448
+ const blob = new Blob([bytes], { type: 'application/pdf' })
449
+ this._pdfObjectUrl = UrlApi.createObjectURL(blob)
450
+ return this._pdfObjectUrl
451
+ }
452
+ return null
453
+ },
454
+ renderNativePDF(container, data) {
455
+ const url = this.pdfDataToUrl(data)
456
+ if (!url) {
457
+ container.textContent = 'Unsupported PDF data format'
458
+ return
459
+ }
460
+ container.innerHTML = ''
461
+ const iframe = document.createElement('iframe')
462
+ iframe.className = 'jsee-pdf-frame'
463
+ iframe.title = this.output.title || this.output.name || 'PDF'
464
+ iframe.src = url
465
+ iframe.setAttribute('loading', 'lazy')
466
+ container.appendChild(iframe)
467
+ },
468
+ renderPDF() {
469
+ if (!this.$refs.pdfContainer) return
470
+ const container = this.$refs.pdfContainer
471
+ const data = this.output.value
472
+ if (!data) return
473
+ this.renderNativePDF(container, data)
474
+ },
139
475
  executeRenderFunction() {
140
476
  if (this.isRenderFunction && this.$refs.customContainer) {
141
477
  // Clear previous content