@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
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
|
}
|
|
@@ -776,12 +888,16 @@ function getName (code) {
|
|
|
776
888
|
switch (typeof code) {
|
|
777
889
|
case 'function':
|
|
778
890
|
if (!code.name) return undefined
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
// Only trust .name when
|
|
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.
|
|
782
894
|
const src = code.toString().trimStart()
|
|
783
|
-
|
|
784
|
-
|
|
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
|
|
785
901
|
}
|
|
786
902
|
return undefined
|
|
787
903
|
case 'string':
|
|
@@ -882,8 +998,9 @@ function validateSchema (schema) {
|
|
|
882
998
|
const hasModel = typeof schema.model !== 'undefined'
|
|
883
999
|
const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
|
|
884
1000
|
|
|
885
|
-
|
|
886
|
-
|
|
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`')
|
|
887
1004
|
} else if (!hasModel && hasView) {
|
|
888
1005
|
report.warnings.push('Schema has no `model`, using `view`/`render` only')
|
|
889
1006
|
}
|
|
@@ -912,8 +1029,12 @@ function validateSchema (schema) {
|
|
|
912
1029
|
|
|
913
1030
|
// Convert a URL parameter string to the appropriate type
|
|
914
1031
|
function coerceParam (value, type, name) {
|
|
915
|
-
if (type === 'number') return Number(value)
|
|
916
|
-
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
|
+
}
|
|
917
1038
|
if (type === 'json') {
|
|
918
1039
|
try { return JSON.parse(value) }
|
|
919
1040
|
catch (e) { console.error(`Failed to parse JSON for input ${name}:`, e) }
|
|
@@ -923,6 +1044,7 @@ function coerceParam (value, type, name) {
|
|
|
923
1044
|
|
|
924
1045
|
// Extract URL parameter value for an input, checking name, sanitized name, and aliases
|
|
925
1046
|
function getUrlParam (urlParams, input) {
|
|
1047
|
+
if (!input.name) return null
|
|
926
1048
|
if (urlParams.has(input.name)) return urlParams.get(input.name)
|
|
927
1049
|
if (urlParams.has(sanitizeName(input.name))) return urlParams.get(sanitizeName(input.name))
|
|
928
1050
|
if (!input.alias) return null
|
|
@@ -933,6 +1055,240 @@ function getUrlParam (urlParams, input) {
|
|
|
933
1055
|
return null
|
|
934
1056
|
}
|
|
935
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
|
+
|
|
936
1292
|
module.exports = {
|
|
937
1293
|
isObject,
|
|
938
1294
|
loadFromDOM,
|
|
@@ -956,5 +1312,19 @@ module.exports = {
|
|
|
956
1312
|
isCssImport,
|
|
957
1313
|
isRelativeImport,
|
|
958
1314
|
getUrlParam,
|
|
959
|
-
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
|
|
960
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
|
}
|