@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
package/src/utils.js
CHANGED
|
@@ -105,7 +105,14 @@ const VALID_INPUT_TYPES = [
|
|
|
105
105
|
'file',
|
|
106
106
|
'group',
|
|
107
107
|
'action',
|
|
108
|
-
'button'
|
|
108
|
+
'button',
|
|
109
|
+
'slider',
|
|
110
|
+
'radio',
|
|
111
|
+
'toggle',
|
|
112
|
+
'date',
|
|
113
|
+
'multi-select',
|
|
114
|
+
'range',
|
|
115
|
+
'folder'
|
|
109
116
|
]
|
|
110
117
|
|
|
111
118
|
const VALID_MODEL_TYPES = [
|
|
@@ -710,7 +717,18 @@ async function importScripts (...imports) {
|
|
|
710
717
|
}
|
|
711
718
|
}
|
|
712
719
|
|
|
713
|
-
function
|
|
720
|
+
function parseSSELine (line) {
|
|
721
|
+
if (!line.startsWith('data:')) return null
|
|
722
|
+
const payload = line.slice(5).trim()
|
|
723
|
+
if (payload === '[DONE]') return null
|
|
724
|
+
try {
|
|
725
|
+
return JSON.parse(payload)
|
|
726
|
+
} catch (e) {
|
|
727
|
+
return payload
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function getModelFuncAPI (model, log=console.log, onChunk) {
|
|
714
732
|
switch (model.type) {
|
|
715
733
|
case 'get':
|
|
716
734
|
return (data) => {
|
|
@@ -724,19 +742,113 @@ function getModelFuncAPI (model, log=console.log) {
|
|
|
724
742
|
case 'post':
|
|
725
743
|
return (data) => {
|
|
726
744
|
log('Sending POST request to', model.url)
|
|
727
|
-
const
|
|
745
|
+
const accept = model.stream ? 'text/event-stream' : 'application/json'
|
|
746
|
+
return fetch(model.url, {
|
|
728
747
|
method: 'POST',
|
|
729
748
|
headers: {
|
|
730
|
-
'Accept':
|
|
749
|
+
'Accept': accept,
|
|
731
750
|
'Content-Type': 'application/json'
|
|
732
751
|
},
|
|
733
752
|
body: JSON.stringify(data)
|
|
734
|
-
}).then(response =>
|
|
735
|
-
|
|
753
|
+
}).then(async (response) => {
|
|
754
|
+
const contentType = response.headers.get('content-type') || ''
|
|
755
|
+
// SSE streaming response
|
|
756
|
+
if (contentType.includes('text/event-stream') && response.body && onChunk) {
|
|
757
|
+
const reader = response.body.getReader()
|
|
758
|
+
const decoder = new TextDecoder()
|
|
759
|
+
let buffer = ''
|
|
760
|
+
let lastResult = null
|
|
761
|
+
while (true) {
|
|
762
|
+
const { done, value } = await reader.read()
|
|
763
|
+
if (done) break
|
|
764
|
+
buffer += decoder.decode(value, { stream: true })
|
|
765
|
+
const lines = buffer.split('\n')
|
|
766
|
+
buffer = lines.pop()
|
|
767
|
+
for (const line of lines) {
|
|
768
|
+
const trimmed = line.trim()
|
|
769
|
+
if (!trimmed) continue
|
|
770
|
+
const parsed = parseSSELine(trimmed)
|
|
771
|
+
if (parsed !== null) {
|
|
772
|
+
lastResult = parsed
|
|
773
|
+
onChunk(parsed)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Process any remaining buffer
|
|
778
|
+
buffer += decoder.decode()
|
|
779
|
+
if (buffer.trim()) {
|
|
780
|
+
const parsed = parseSSELine(buffer.trim())
|
|
781
|
+
if (parsed !== null) {
|
|
782
|
+
lastResult = parsed
|
|
783
|
+
onChunk(parsed)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return lastResult
|
|
787
|
+
}
|
|
788
|
+
return response.json()
|
|
789
|
+
})
|
|
736
790
|
}
|
|
737
791
|
}
|
|
738
792
|
}
|
|
739
793
|
|
|
794
|
+
const TYPED_ARRAY_CONSTRUCTORS = {
|
|
795
|
+
float32: Float32Array,
|
|
796
|
+
float64: Float64Array,
|
|
797
|
+
int8: Int8Array,
|
|
798
|
+
int16: Int16Array,
|
|
799
|
+
int32: Int32Array,
|
|
800
|
+
uint8: Uint8Array,
|
|
801
|
+
uint16: Uint16Array,
|
|
802
|
+
uint32: Uint32Array,
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function toTypedArray (value, dtype) {
|
|
806
|
+
if (!dtype || !TYPED_ARRAY_CONSTRUCTORS[dtype]) return value
|
|
807
|
+
const Ctor = TYPED_ARRAY_CONSTRUCTORS[dtype]
|
|
808
|
+
if (value instanceof Ctor) return value
|
|
809
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
810
|
+
return new Ctor(value instanceof ArrayBuffer ? value : value.buffer)
|
|
811
|
+
}
|
|
812
|
+
if (Array.isArray(value)) return new Ctor(value)
|
|
813
|
+
return value
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function fromTypedArray (value) {
|
|
817
|
+
if (ArrayBuffer.isView(value)) return Array.from(value)
|
|
818
|
+
return value
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function collectTransferables (value, seen) {
|
|
822
|
+
if (!value || typeof value !== 'object') return []
|
|
823
|
+
if (!seen) seen = new WeakSet()
|
|
824
|
+
if (seen.has(value)) return []
|
|
825
|
+
seen.add(value)
|
|
826
|
+
const buffers = []
|
|
827
|
+
if (value instanceof ArrayBuffer) {
|
|
828
|
+
buffers.push(value)
|
|
829
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
830
|
+
buffers.push(value.buffer)
|
|
831
|
+
} else if (Array.isArray(value)) {
|
|
832
|
+
value.forEach(item => buffers.push(...collectTransferables(item, seen)))
|
|
833
|
+
} else {
|
|
834
|
+
Object.keys(value).forEach(key => {
|
|
835
|
+
buffers.push(...collectTransferables(value[key], seen))
|
|
836
|
+
})
|
|
837
|
+
}
|
|
838
|
+
return buffers
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function wrapTypedArrayInputs (inputs, inputConfigs) {
|
|
842
|
+
if (!isObject(inputs) || !Array.isArray(inputConfigs)) return inputs
|
|
843
|
+
const wrapped = Object.assign({}, inputs)
|
|
844
|
+
inputConfigs.forEach(cfg => {
|
|
845
|
+
if (cfg.arrayBuffer && cfg.name && typeof wrapped[cfg.name] !== 'undefined') {
|
|
846
|
+
wrapped[cfg.name] = toTypedArray(wrapped[cfg.name], cfg.dtype || 'float64')
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
return wrapped
|
|
850
|
+
}
|
|
851
|
+
|
|
740
852
|
async function delay (ms) {
|
|
741
853
|
return new Promise(resolve => setTimeout(resolve, ms || 1))
|
|
742
854
|
}
|
|
@@ -775,7 +887,19 @@ function debounce (fn, ms) {
|
|
|
775
887
|
function getName (code) {
|
|
776
888
|
switch (typeof code) {
|
|
777
889
|
case 'function':
|
|
778
|
-
|
|
890
|
+
if (!code.name) return undefined
|
|
891
|
+
// JS infers .name from property assignment for ALL function forms:
|
|
892
|
+
// { code: (a) => a } and { code: function(a) {} } both get .name === "code".
|
|
893
|
+
// Only trust .name when the name actually appears in the source text.
|
|
894
|
+
const src = code.toString().trimStart()
|
|
895
|
+
const keyword = src.startsWith('async function') ? 'async function'
|
|
896
|
+
: src.startsWith('function') ? 'function'
|
|
897
|
+
: null
|
|
898
|
+
if (keyword) {
|
|
899
|
+
const afterKeyword = src.slice(keyword.length).trimStart()
|
|
900
|
+
if (afterKeyword.startsWith(code.name)) return code.name
|
|
901
|
+
}
|
|
902
|
+
return undefined
|
|
779
903
|
case 'string':
|
|
780
904
|
const words = code.split(' ')
|
|
781
905
|
const functionIndex = words.findIndex((word) => word === 'function')
|
|
@@ -874,8 +998,9 @@ function validateSchema (schema) {
|
|
|
874
998
|
const hasModel = typeof schema.model !== 'undefined'
|
|
875
999
|
const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
|
|
876
1000
|
|
|
877
|
-
|
|
878
|
-
|
|
1001
|
+
const hasInputs = Array.isArray(schema.inputs) && schema.inputs.length > 0
|
|
1002
|
+
if (!hasModel && !hasView && !hasInputs) {
|
|
1003
|
+
report.errors.push('Schema should define `model` (or `view`/`render`) or `inputs`')
|
|
879
1004
|
} else if (!hasModel && hasView) {
|
|
880
1005
|
report.warnings.push('Schema has no `model`, using `view`/`render` only')
|
|
881
1006
|
}
|
|
@@ -904,8 +1029,12 @@ function validateSchema (schema) {
|
|
|
904
1029
|
|
|
905
1030
|
// Convert a URL parameter string to the appropriate type
|
|
906
1031
|
function coerceParam (value, type, name) {
|
|
907
|
-
if (type === 'number') return Number(value)
|
|
908
|
-
if (type === 'boolean') return value === 'true'
|
|
1032
|
+
if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') return Number(value)
|
|
1033
|
+
if (type === 'boolean' || type === 'bool' || type === 'checkbox' || type === 'toggle') return value === 'true'
|
|
1034
|
+
if (type === 'range') {
|
|
1035
|
+
try { return JSON.parse(value) }
|
|
1036
|
+
catch (e) { console.error(`Failed to parse range for input ${name}:`, e) }
|
|
1037
|
+
}
|
|
909
1038
|
if (type === 'json') {
|
|
910
1039
|
try { return JSON.parse(value) }
|
|
911
1040
|
catch (e) { console.error(`Failed to parse JSON for input ${name}:`, e) }
|
|
@@ -915,6 +1044,7 @@ function coerceParam (value, type, name) {
|
|
|
915
1044
|
|
|
916
1045
|
// Extract URL parameter value for an input, checking name, sanitized name, and aliases
|
|
917
1046
|
function getUrlParam (urlParams, input) {
|
|
1047
|
+
if (!input.name) return null
|
|
918
1048
|
if (urlParams.has(input.name)) return urlParams.get(input.name)
|
|
919
1049
|
if (urlParams.has(sanitizeName(input.name))) return urlParams.get(sanitizeName(input.name))
|
|
920
1050
|
if (!input.alias) return null
|
|
@@ -925,6 +1055,240 @@ function getUrlParam (urlParams, input) {
|
|
|
925
1055
|
return null
|
|
926
1056
|
}
|
|
927
1057
|
|
|
1058
|
+
function jseeInputsToJsonSchema (inputs) {
|
|
1059
|
+
const properties = {}
|
|
1060
|
+
const required = []
|
|
1061
|
+
for (const inp of (inputs || [])) {
|
|
1062
|
+
const prop = {}
|
|
1063
|
+
if (inp.description) prop.description = inp.description
|
|
1064
|
+
switch (inp.type) {
|
|
1065
|
+
case 'int':
|
|
1066
|
+
prop.type = 'integer'
|
|
1067
|
+
break
|
|
1068
|
+
case 'float': case 'number':
|
|
1069
|
+
prop.type = 'number'
|
|
1070
|
+
break
|
|
1071
|
+
case 'bool': case 'checkbox': case 'toggle':
|
|
1072
|
+
prop.type = 'boolean'
|
|
1073
|
+
break
|
|
1074
|
+
case 'select': case 'categorical': case 'radio':
|
|
1075
|
+
prop.type = 'string'
|
|
1076
|
+
if (inp.options) prop.enum = inp.options
|
|
1077
|
+
break
|
|
1078
|
+
case 'slider':
|
|
1079
|
+
prop.type = 'number'
|
|
1080
|
+
if (inp.min !== undefined) prop.minimum = inp.min
|
|
1081
|
+
if (inp.max !== undefined) prop.maximum = inp.max
|
|
1082
|
+
if (inp.step !== undefined) prop.multipleOf = inp.step
|
|
1083
|
+
break
|
|
1084
|
+
case 'range':
|
|
1085
|
+
prop.type = 'array'
|
|
1086
|
+
prop.items = { type: 'number' }
|
|
1087
|
+
prop.minItems = 2
|
|
1088
|
+
prop.maxItems = 2
|
|
1089
|
+
break
|
|
1090
|
+
case 'multi-select':
|
|
1091
|
+
prop.type = 'array'
|
|
1092
|
+
prop.items = { type: 'string' }
|
|
1093
|
+
if (inp.options) prop.items.enum = inp.options
|
|
1094
|
+
break
|
|
1095
|
+
default:
|
|
1096
|
+
prop.type = 'string'
|
|
1097
|
+
}
|
|
1098
|
+
if (inp.default !== undefined) prop.default = inp.default
|
|
1099
|
+
properties[inp.name] = prop
|
|
1100
|
+
if (inp.default === undefined) required.push(inp.name)
|
|
1101
|
+
}
|
|
1102
|
+
return { type: 'object', properties, required }
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function generateOpenAPISpec (schema) {
|
|
1106
|
+
const models = Array.isArray(schema.model) ? schema.model : (schema.model ? [schema.model] : [])
|
|
1107
|
+
const paths = {}
|
|
1108
|
+
const inputSchema = jseeInputsToJsonSchema(schema.inputs)
|
|
1109
|
+
|
|
1110
|
+
for (const m of models) {
|
|
1111
|
+
paths['/' + m.name] = {
|
|
1112
|
+
post: {
|
|
1113
|
+
summary: 'Run ' + m.name,
|
|
1114
|
+
operationId: m.name,
|
|
1115
|
+
requestBody: {
|
|
1116
|
+
required: true,
|
|
1117
|
+
content: { 'application/json': { schema: inputSchema } }
|
|
1118
|
+
},
|
|
1119
|
+
responses: {
|
|
1120
|
+
'200': {
|
|
1121
|
+
description: 'Model output',
|
|
1122
|
+
content: { 'application/json': { schema: { type: 'object' } } }
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const title = schema.title
|
|
1130
|
+
|| (schema.page && schema.page.title)
|
|
1131
|
+
|| (models[0] && models[0].name)
|
|
1132
|
+
|| 'JSEE API'
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
openapi: '3.1.0',
|
|
1136
|
+
info: { title, version: '1.0.0' },
|
|
1137
|
+
paths
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function serializeResult (result) {
|
|
1142
|
+
if (result === null || result === undefined) return { result: null }
|
|
1143
|
+
// Buffer or Uint8Array → base64 image
|
|
1144
|
+
if (Buffer.isBuffer(result) || result instanceof Uint8Array) {
|
|
1145
|
+
const b64 = Buffer.from(result).toString('base64')
|
|
1146
|
+
return { result: 'data:image/png;base64,' + b64 }
|
|
1147
|
+
}
|
|
1148
|
+
// Plain object or array — return as-is
|
|
1149
|
+
if (typeof result === 'object') return result
|
|
1150
|
+
// Primitives
|
|
1151
|
+
return { result }
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function parseMultipart (contentType, body) {
|
|
1155
|
+
const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/)
|
|
1156
|
+
if (!match) return {}
|
|
1157
|
+
const boundary = '--' + (match[1] || match[2])
|
|
1158
|
+
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body)
|
|
1159
|
+
const data = {}
|
|
1160
|
+
let start = buf.indexOf(boundary) + boundary.length
|
|
1161
|
+
while (start < buf.length) {
|
|
1162
|
+
// Skip \r\n after boundary
|
|
1163
|
+
if (buf[start] === 0x0d) start += 2
|
|
1164
|
+
else if (buf[start] === 0x0a) start += 1
|
|
1165
|
+
// Check for closing boundary (--)
|
|
1166
|
+
if (buf[start] === 0x2d && buf[start + 1] === 0x2d) break
|
|
1167
|
+
// Find end of headers (\r\n\r\n)
|
|
1168
|
+
const headerEnd = buf.indexOf('\r\n\r\n', start)
|
|
1169
|
+
if (headerEnd === -1) break
|
|
1170
|
+
const headers = buf.slice(start, headerEnd).toString('utf-8')
|
|
1171
|
+
const bodyStart = headerEnd + 4
|
|
1172
|
+
// Find next boundary
|
|
1173
|
+
const nextBoundary = buf.indexOf(boundary, bodyStart)
|
|
1174
|
+
if (nextBoundary === -1) break
|
|
1175
|
+
// Body ends 2 bytes before boundary (\r\n)
|
|
1176
|
+
const bodyEnd = nextBoundary - 2
|
|
1177
|
+
const nameMatch = headers.match(/name="([^"]+)"/)
|
|
1178
|
+
if (nameMatch) {
|
|
1179
|
+
const name = nameMatch[1]
|
|
1180
|
+
const filenameMatch = headers.match(/filename="([^"]*)"/)
|
|
1181
|
+
if (filenameMatch) {
|
|
1182
|
+
// File field — keep as Buffer
|
|
1183
|
+
data[name] = buf.slice(bodyStart, bodyEnd)
|
|
1184
|
+
} else {
|
|
1185
|
+
// Text field — try to parse as JSON for numbers/booleans
|
|
1186
|
+
const val = buf.slice(bodyStart, bodyEnd).toString('utf-8')
|
|
1187
|
+
try { data[name] = JSON.parse(val) } catch (e) { data[name] = val }
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
start = nextBoundary + boundary.length
|
|
1191
|
+
}
|
|
1192
|
+
return data
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Convert column-oriented data {x: [1,2], y: [3,4]} to row-oriented [{x:1, y:3}, {x:2, y:4}]
|
|
1196
|
+
function columnsToRows (data) {
|
|
1197
|
+
if (!isObject(data)) return data
|
|
1198
|
+
const keys = Object.keys(data)
|
|
1199
|
+
if (keys.length === 0) return []
|
|
1200
|
+
const firstArray = data[keys[0]]
|
|
1201
|
+
if (!Array.isArray(firstArray)) return data
|
|
1202
|
+
// Ensure all values are arrays of the same length
|
|
1203
|
+
const len = firstArray.length
|
|
1204
|
+
if (!keys.every(k => Array.isArray(data[k]) && data[k].length === len)) return data
|
|
1205
|
+
const rows = []
|
|
1206
|
+
for (let i = 0; i < len; i++) {
|
|
1207
|
+
const row = {}
|
|
1208
|
+
keys.forEach(k => { row[k] = data[k][i] })
|
|
1209
|
+
rows.push(row)
|
|
1210
|
+
}
|
|
1211
|
+
return rows
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function createValidateFn (input, filtrexCompile, filtrexOptions) {
|
|
1215
|
+
if (input.validate) {
|
|
1216
|
+
const expr = input.validate.replace(/\'/g, '"')
|
|
1217
|
+
const f = filtrexCompile(expr, filtrexOptions)
|
|
1218
|
+
const msg = input.error || 'Invalid value'
|
|
1219
|
+
return function (value) {
|
|
1220
|
+
try {
|
|
1221
|
+
return f({ value }) ? null : msg
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
return msg
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
} else if (input.required) {
|
|
1227
|
+
const msg = input.error || 'Required'
|
|
1228
|
+
return function (value) {
|
|
1229
|
+
if (value === null || value === undefined || value === '') return msg
|
|
1230
|
+
if (Array.isArray(value) && value.length === 0) return msg
|
|
1231
|
+
return null
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return null
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function runValidation (inputs, validateFunctions) {
|
|
1238
|
+
let hasErrors = false
|
|
1239
|
+
inputs.forEach((input, index) => {
|
|
1240
|
+
if (index < validateFunctions.length && validateFunctions[index]) {
|
|
1241
|
+
input._error = validateFunctions[index](input.value)
|
|
1242
|
+
if (input._error) hasErrors = true
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1245
|
+
return hasErrors
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const EXT_TO_OUTPUT = {
|
|
1249
|
+
'.pdf': 'pdf',
|
|
1250
|
+
'.png': 'image', '.jpg': 'image', '.jpeg': 'image',
|
|
1251
|
+
'.gif': 'image', '.svg': 'image', '.webp': 'image',
|
|
1252
|
+
'.mp3': 'audio', '.wav': 'audio', '.ogg': 'audio', '.flac': 'audio',
|
|
1253
|
+
'.mp4': 'video', '.webm': 'video', '.mov': 'video',
|
|
1254
|
+
'.csv': 'table', '.tsv': 'table',
|
|
1255
|
+
'.md': 'markdown',
|
|
1256
|
+
'.html': 'html', '.htm': 'html',
|
|
1257
|
+
'.json': 'object',
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function fileExtToOutputType (filename) {
|
|
1261
|
+
if (typeof filename !== 'string') return 'code'
|
|
1262
|
+
const dotIdx = filename.lastIndexOf('.')
|
|
1263
|
+
if (dotIdx < 0) return 'code'
|
|
1264
|
+
const ext = filename.slice(dotIdx).toLowerCase().replace(/[?#].*$/, '')
|
|
1265
|
+
return EXT_TO_OUTPUT[ext] || 'code'
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function inferOutputType (key, value) {
|
|
1269
|
+
if (Array.isArray(value)) {
|
|
1270
|
+
if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) return 'table'
|
|
1271
|
+
if (value.length > 0 && typeof value[0] === 'string' && /\.(png|jpe?g|gif|svg|webp)([?#].*)?$/i.test(value[0])) return 'gallery'
|
|
1272
|
+
return 'object'
|
|
1273
|
+
}
|
|
1274
|
+
if (typeof value === 'string') {
|
|
1275
|
+
if (value.startsWith('data:image/')) return 'image'
|
|
1276
|
+
if (value.startsWith('data:audio/')) return 'audio'
|
|
1277
|
+
if (value.startsWith('data:video/')) return 'video'
|
|
1278
|
+
if (value.startsWith('data:application/pdf')) return 'pdf'
|
|
1279
|
+
if (/\.(png|jpe?g|gif|svg|webp)([?#].*)?$/i.test(value)) return 'image'
|
|
1280
|
+
if (/\.(mp3|wav|ogg|flac)([?#].*)?$/i.test(value)) return 'audio'
|
|
1281
|
+
if (/\.(mp4|webm|mov)([?#].*)?$/i.test(value)) return 'video'
|
|
1282
|
+
if (/\.pdf([?#].*)?$/i.test(value)) return 'pdf'
|
|
1283
|
+
if (/\.md([?#].*)?$/i.test(value)) return 'markdown'
|
|
1284
|
+
if (value.includes('\n') && value.length > 200) return 'code'
|
|
1285
|
+
return 'string'
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof value === 'number' || typeof value === 'boolean') return 'string'
|
|
1288
|
+
if (typeof value === 'object' && value !== null) return 'object'
|
|
1289
|
+
return 'string'
|
|
1290
|
+
}
|
|
1291
|
+
|
|
928
1292
|
module.exports = {
|
|
929
1293
|
isObject,
|
|
930
1294
|
loadFromDOM,
|
|
@@ -948,5 +1312,19 @@ module.exports = {
|
|
|
948
1312
|
isCssImport,
|
|
949
1313
|
isRelativeImport,
|
|
950
1314
|
getUrlParam,
|
|
951
|
-
coerceParam
|
|
1315
|
+
coerceParam,
|
|
1316
|
+
jseeInputsToJsonSchema,
|
|
1317
|
+
generateOpenAPISpec,
|
|
1318
|
+
serializeResult,
|
|
1319
|
+
parseMultipart,
|
|
1320
|
+
parseSSELine,
|
|
1321
|
+
toTypedArray,
|
|
1322
|
+
fromTypedArray,
|
|
1323
|
+
wrapTypedArrayInputs,
|
|
1324
|
+
collectTransferables,
|
|
1325
|
+
columnsToRows,
|
|
1326
|
+
createValidateFn,
|
|
1327
|
+
runValidation,
|
|
1328
|
+
fileExtToOutputType,
|
|
1329
|
+
inferOutputType
|
|
952
1330
|
}
|
|
@@ -5,14 +5,102 @@ const component = {
|
|
|
5
5
|
props: ['input'],
|
|
6
6
|
emits: ['inchange'],
|
|
7
7
|
components: { FilePicker },
|
|
8
|
+
data () {
|
|
9
|
+
return {
|
|
10
|
+
collapsed: this.input && this.input.collapsed === true,
|
|
11
|
+
activeTab: 0
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
computed: {
|
|
15
|
+
effectiveStyle () {
|
|
16
|
+
if (this.input.style) return this.input.style
|
|
17
|
+
if (this.input.collapsed !== undefined || this.input.label) return 'accordion'
|
|
18
|
+
return 'blocks'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
8
21
|
methods: {
|
|
9
22
|
changeHandler () {
|
|
10
23
|
if (this.input.reactive) {
|
|
11
24
|
this.$emit('inchange')
|
|
12
25
|
}
|
|
13
26
|
},
|
|
27
|
+
toggleCollapsed () {
|
|
28
|
+
this.collapsed = !this.collapsed
|
|
29
|
+
},
|
|
30
|
+
autosize (e) {
|
|
31
|
+
const el = e.target
|
|
32
|
+
el.style.height = 'auto'
|
|
33
|
+
el.style.height = el.scrollHeight + 'px'
|
|
34
|
+
},
|
|
14
35
|
call (method) {
|
|
15
36
|
console.log('calling: ', method)
|
|
37
|
+
},
|
|
38
|
+
folderSelected (e) {
|
|
39
|
+
const files = Array.from(e.target.files)
|
|
40
|
+
this.input.value = files.map(f => ({
|
|
41
|
+
name: f.webkitRelativePath || f.name,
|
|
42
|
+
path: f.webkitRelativePath || f.name,
|
|
43
|
+
size: f.size,
|
|
44
|
+
type: f.type,
|
|
45
|
+
selected: true,
|
|
46
|
+
_file: f
|
|
47
|
+
}))
|
|
48
|
+
this.changeHandler()
|
|
49
|
+
},
|
|
50
|
+
folderDropped (e) {
|
|
51
|
+
const files = Array.from(e.dataTransfer.files)
|
|
52
|
+
this.input.value = files.map(f => ({
|
|
53
|
+
name: f.name,
|
|
54
|
+
path: f.name,
|
|
55
|
+
size: f.size,
|
|
56
|
+
type: f.type,
|
|
57
|
+
selected: true,
|
|
58
|
+
_file: f
|
|
59
|
+
}))
|
|
60
|
+
this.changeHandler()
|
|
61
|
+
},
|
|
62
|
+
rangeInput (e, which) {
|
|
63
|
+
const val = Number(e.target.value)
|
|
64
|
+
const v = this.input.value || [0, 100]
|
|
65
|
+
if (v[0] === v[1]) {
|
|
66
|
+
// Same point: direction determines which thumb moves
|
|
67
|
+
if (val > v[0]) this.input.value = [v[0], val]
|
|
68
|
+
else if (val < v[0]) this.input.value = [val, v[1]]
|
|
69
|
+
} else if (which === 'min') {
|
|
70
|
+
this.input.value = [Math.min(val, v[1]), v[1]]
|
|
71
|
+
} else {
|
|
72
|
+
this.input.value = [v[0], Math.max(val, v[0])]
|
|
73
|
+
}
|
|
74
|
+
this.changeHandler()
|
|
75
|
+
},
|
|
76
|
+
folderSelectOne (file) {
|
|
77
|
+
this.input.value.forEach(f => { f.selected = false })
|
|
78
|
+
file.selected = true
|
|
79
|
+
if (this.input.reactive) {
|
|
80
|
+
this.$emit('inchange')
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
folderSelectionChanged () {
|
|
84
|
+
if (this.input.reactive) {
|
|
85
|
+
this.$emit('inchange')
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
formatSize (bytes) {
|
|
89
|
+
if (!bytes) return ''
|
|
90
|
+
if (bytes < 1024) return bytes + ' B'
|
|
91
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
92
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
mounted () {
|
|
96
|
+
if (this.input.type === 'text' && this.input.value) {
|
|
97
|
+
this.$nextTick(() => {
|
|
98
|
+
const el = this.$el.querySelector('textarea')
|
|
99
|
+
if (el) {
|
|
100
|
+
el.style.height = 'auto'
|
|
101
|
+
el.style.height = el.scrollHeight + 'px'
|
|
102
|
+
}
|
|
103
|
+
})
|
|
16
104
|
}
|
|
17
105
|
}
|
|
18
106
|
}
|