@flowfuse/nr-assistant 0.7.0 → 0.7.1-1db256d-202601141503.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/index.html +287 -4
- package/lib/assistant.js +1 -0
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
enabled: false,
|
|
46
46
|
tablesEnabled: false,
|
|
47
47
|
inlineCompletionsEnabled: false,
|
|
48
|
-
requestTimeout: AI_TIMEOUT
|
|
48
|
+
requestTimeout: AI_TIMEOUT,
|
|
49
|
+
assistantVersion: null
|
|
49
50
|
}
|
|
50
51
|
let initialisedInterlock = false
|
|
51
52
|
let mcpReadyInterlock = false
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
RED.comms.subscribe('nr-assistant/#', (topic, msg) => {
|
|
63
64
|
debug('comms', topic, msg)
|
|
64
65
|
if (topic === 'nr-assistant/initialise') {
|
|
66
|
+
assistantOptions.assistantVersion = msg?.assistantVersion
|
|
65
67
|
assistantOptions.standalone = !!msg?.standalone
|
|
66
68
|
assistantOptions.enabled = !!msg?.enabled
|
|
67
69
|
assistantOptions.requestTimeout = msg?.requestTimeout || AI_TIMEOUT
|
|
@@ -198,6 +200,164 @@
|
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
if (!assistantInitialised) {
|
|
203
|
+
// Setup postMessage communication with parent window
|
|
204
|
+
if (!window.parent?.postMessage || window.self === window.top) {
|
|
205
|
+
console.warn('Parent window not detected - certain interactions with the FlowFuse Expert will not be available')
|
|
206
|
+
} else {
|
|
207
|
+
const MESSAGE_SOURCE = 'nr-assistant'
|
|
208
|
+
const MESSAGE_TARGET = 'flowfuse-expert'
|
|
209
|
+
const MESSAGE_SCOPE = 'flowfuse-expert'
|
|
210
|
+
|
|
211
|
+
// proxy certain events from RED Events to the parent window (for state tracking)
|
|
212
|
+
RED.events.on('editor:open', function () {
|
|
213
|
+
window.parent.postMessage({
|
|
214
|
+
type: 'editor:open',
|
|
215
|
+
source: MESSAGE_SOURCE,
|
|
216
|
+
scope: MESSAGE_SCOPE,
|
|
217
|
+
target: MESSAGE_TARGET
|
|
218
|
+
}, '*')
|
|
219
|
+
})
|
|
220
|
+
RED.events.on('editor:close', function () {
|
|
221
|
+
window.parent.postMessage({
|
|
222
|
+
type: 'editor:close',
|
|
223
|
+
source: MESSAGE_SOURCE,
|
|
224
|
+
scope: MESSAGE_SCOPE,
|
|
225
|
+
target: MESSAGE_TARGET
|
|
226
|
+
}, '*')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Define supported actions and their parameter schemas
|
|
230
|
+
const supportedActions = {
|
|
231
|
+
'core:manage-palette': {
|
|
232
|
+
params: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
view: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
enum: ['nodes', 'install'],
|
|
238
|
+
default: 'install'
|
|
239
|
+
},
|
|
240
|
+
filter: {
|
|
241
|
+
description: 'Optional filter string. e.g. `"node-red-contrib-s7","node-red-contrib-other"` to pre-filter the palette view',
|
|
242
|
+
type: 'string'
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
required: ['filter']
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
'custom:import-flow': {
|
|
249
|
+
params: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
flow: {
|
|
253
|
+
type: 'string',
|
|
254
|
+
description: 'The flow JSON to import'
|
|
255
|
+
},
|
|
256
|
+
addFlow: {
|
|
257
|
+
type: 'boolean',
|
|
258
|
+
description: 'Whether to add the flow to the current workspace tab (false) or create a new tab (true). Default: false'
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
required: ['flow']
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Listen for postMessages from parent window
|
|
267
|
+
window.addEventListener('message', function (event) {
|
|
268
|
+
// prevent own messages being processed
|
|
269
|
+
if (event.source === window.self) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { type, action, params, target, source, scope } = event.data || {}
|
|
274
|
+
|
|
275
|
+
// Ensure scope and source match expected values
|
|
276
|
+
if (target !== MESSAGE_SOURCE || source !== MESSAGE_TARGET || scope !== MESSAGE_SCOPE) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
debug('Received postMessage:', event.data)
|
|
281
|
+
|
|
282
|
+
const postReply = (message) => {
|
|
283
|
+
debug('Posting reply message:', message)
|
|
284
|
+
if (event.source && typeof event.source.postMessage === 'function') {
|
|
285
|
+
event.source.postMessage({
|
|
286
|
+
...message,
|
|
287
|
+
source: MESSAGE_SOURCE,
|
|
288
|
+
scope: MESSAGE_SCOPE,
|
|
289
|
+
target: MESSAGE_TARGET
|
|
290
|
+
}, event.origin)
|
|
291
|
+
} else {
|
|
292
|
+
console.warn('Unable to post message reply, source not available', message)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// handle version request
|
|
297
|
+
if (type === 'get-assistant-version') {
|
|
298
|
+
// Reply with the current version
|
|
299
|
+
postReply({ type, version: assistantOptions.assistantVersion, success: true }, event.origin)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
// handle supported actions request
|
|
303
|
+
if (type === 'get-supported-actions') {
|
|
304
|
+
// Reply with the supported actions and their schemas
|
|
305
|
+
postReply({ type, supportedActions, success: true }, event.origin)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// handle action invocation requests (must be registered actions in supportedActions)
|
|
310
|
+
if (type === 'invoke-action' && typeof action === 'string') {
|
|
311
|
+
if (!supportedActions[action]) {
|
|
312
|
+
console.warn(`Action "${action}" is not permitted to be invoked via postMessage`)
|
|
313
|
+
postReply({ type, action, error: 'unknown-action' })
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
// Validate params against permitted schema (native/naive parsing for now - may introduce a library later if more complex schemas are needed)
|
|
317
|
+
const actionSchema = supportedActions[action].params
|
|
318
|
+
if (actionSchema) {
|
|
319
|
+
const validation = validateSchema(params, actionSchema)
|
|
320
|
+
if (!validation || !validation.valid) {
|
|
321
|
+
console.warn(`Params for action "${action}" did not validate against the expected schema`, params, actionSchema, validation)
|
|
322
|
+
postReply({ type, action, error: validation.error || 'invalid-parameters' })
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (action === 'custom:import-flow') {
|
|
328
|
+
// import-flow is a custom action - handle it here directly
|
|
329
|
+
try {
|
|
330
|
+
importNodes(params.flow, params.addFlow === true)
|
|
331
|
+
postReply({ type, success: true })
|
|
332
|
+
} catch (err) {
|
|
333
|
+
postReply({ type, error: err?.message })
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// Handle (supported) native Node-RED actions
|
|
337
|
+
try {
|
|
338
|
+
RED.actions.invoke(action, params)
|
|
339
|
+
postReply({ type, action, success: true })
|
|
340
|
+
} catch (err) {
|
|
341
|
+
postReply({ type, action, error: err?.message })
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// unknown message type
|
|
348
|
+
postReply({ type: 'error', error: 'unknown-type', data: event.data })
|
|
349
|
+
}, false)
|
|
350
|
+
|
|
351
|
+
// Notify the parent window that the assistant is ready
|
|
352
|
+
window.parent.postMessage({
|
|
353
|
+
type: 'assistant-ready',
|
|
354
|
+
source: MESSAGE_SOURCE,
|
|
355
|
+
scope: MESSAGE_SCOPE,
|
|
356
|
+
target: MESSAGE_TARGET,
|
|
357
|
+
version: assistantOptions.assistantVersion
|
|
358
|
+
}, '*')
|
|
359
|
+
}
|
|
360
|
+
|
|
201
361
|
registerMonacoExtensions()
|
|
202
362
|
RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:function-builder.action.label' })
|
|
203
363
|
setMenuShortcutKey('ff-assistant-function-builder', 'red-ui-workspace', 'ctrl-alt-f', 'flowfuse-nr-assistant:function-builder')
|
|
@@ -832,7 +992,7 @@
|
|
|
832
992
|
nodeType: node.type,
|
|
833
993
|
nodeModule: node._def?.set?.module || 'node-red'
|
|
834
994
|
}
|
|
835
|
-
|
|
995
|
+
|
|
836
996
|
context.outputs = +(node.outputs || 1)
|
|
837
997
|
if (isNaN(context.outputs) || context.outputs < 1) {
|
|
838
998
|
context.outputs = 1
|
|
@@ -1956,6 +2116,129 @@
|
|
|
1956
2116
|
}
|
|
1957
2117
|
RED.menu.refreshShortcuts()
|
|
1958
2118
|
}
|
|
2119
|
+
|
|
2120
|
+
/**
|
|
2121
|
+
* Validates data against a simple schema
|
|
2122
|
+
* NOTE: This is a very basic implementation and only supports a subset of JSON Schema for now.
|
|
2123
|
+
* @param {any} data - The data to validate
|
|
2124
|
+
* @param {object} schema - The schema to validate against
|
|
2125
|
+
* @returns {{valid: boolean, error?: string}} - The validation result
|
|
2126
|
+
*/
|
|
2127
|
+
function validateSchema (data, schema) {
|
|
2128
|
+
if (schema.type === 'object') {
|
|
2129
|
+
if (typeof data !== 'object') {
|
|
2130
|
+
return {
|
|
2131
|
+
valid: false,
|
|
2132
|
+
error: 'Data is not of type object'
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (Array.isArray(data)) {
|
|
2136
|
+
return {
|
|
2137
|
+
valid: false,
|
|
2138
|
+
error: 'Data is an array but an object was expected'
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
// check required properties
|
|
2142
|
+
if (Array.isArray(schema.required)) {
|
|
2143
|
+
for (const reqProp of schema.required) {
|
|
2144
|
+
if (!(reqProp in data)) {
|
|
2145
|
+
return {
|
|
2146
|
+
valid: false,
|
|
2147
|
+
error: `Data is missing required parameter "${reqProp}"`
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
// check properties & apply defaults
|
|
2153
|
+
if (schema.properties) {
|
|
2154
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
2155
|
+
const propExists = propName in data
|
|
2156
|
+
// check type
|
|
2157
|
+
if (propSchema.type && propExists) {
|
|
2158
|
+
const expectedType = propSchema.type
|
|
2159
|
+
const actualType = Array.isArray(data[propName]) ? 'array' : typeof data[propName]
|
|
2160
|
+
if (actualType !== expectedType) {
|
|
2161
|
+
return {
|
|
2162
|
+
valid: false,
|
|
2163
|
+
error: `Data parameter "${propName}" is of type "${actualType}" but expected type is "${expectedType}"`
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// check enum
|
|
2168
|
+
if (propSchema.enum && propExists) {
|
|
2169
|
+
if (!propSchema.enum.includes(data[propName])) {
|
|
2170
|
+
return {
|
|
2171
|
+
valid: false,
|
|
2172
|
+
error: `Data parameter "${propName}" has invalid value "${data[propName]}". Should be one of: ${propSchema.enum.join(', ')}`
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
// apply defaults
|
|
2177
|
+
if (propSchema.default !== undefined && !propExists) {
|
|
2178
|
+
data[propName] = propSchema.default
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return { valid: true }
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/// Function extracted from Node-RED source `editor-client/src/js/ui/clipboard.js`
|
|
2187
|
+
/**
|
|
2188
|
+
* Performs the import of nodes, handling any conflicts that may arise
|
|
2189
|
+
* @param {string} nodesStr the nodes to import as a string
|
|
2190
|
+
* @param {boolean} addFlow whether to add the nodes to a new flow or to the current flow
|
|
2191
|
+
*/
|
|
2192
|
+
function importNodes (nodesStr, addFlow) {
|
|
2193
|
+
let newNodes = nodesStr
|
|
2194
|
+
if (typeof nodesStr === 'string') {
|
|
2195
|
+
try {
|
|
2196
|
+
nodesStr = nodesStr.trim()
|
|
2197
|
+
if (nodesStr.length === 0) {
|
|
2198
|
+
return
|
|
2199
|
+
}
|
|
2200
|
+
newNodes = validateFlowString(nodesStr)
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
const e = new Error(RED._('clipboard.invalidFlow', { message: 'test' }))
|
|
2203
|
+
e.code = 'NODE_RED'
|
|
2204
|
+
throw e
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const importOptions = { generateIds: true, addFlow }
|
|
2208
|
+
try {
|
|
2209
|
+
RED.view.importNodes(newNodes, importOptions)
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
// Thrown for import_conflict
|
|
2212
|
+
RED.notify('Import failed:' + error.message, 'error')
|
|
2213
|
+
throw error
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/// Function extracted from Node-RED source `editor-client/src/js/ui/clipboard.js`
|
|
2218
|
+
/**
|
|
2219
|
+
* Validates if the provided string looks like valid flow json
|
|
2220
|
+
* @param {string} flowString the string to validate
|
|
2221
|
+
* @returns If valid, returns the node array
|
|
2222
|
+
*/
|
|
2223
|
+
function validateFlowString (flowString) {
|
|
2224
|
+
const res = JSON.parse(flowString)
|
|
2225
|
+
if (!Array.isArray(res)) {
|
|
2226
|
+
throw new Error(RED._('clipboard.import.errors.notArray'))
|
|
2227
|
+
}
|
|
2228
|
+
for (let i = 0; i < res.length; i++) {
|
|
2229
|
+
if (typeof res[i] !== 'object') {
|
|
2230
|
+
throw new Error(RED._('clipboard.import.errors.itemNotObject', { index: i }))
|
|
2231
|
+
}
|
|
2232
|
+
if (!Object.hasOwn(res[i], 'id')) {
|
|
2233
|
+
throw new Error(RED._('clipboard.import.errors.missingId', { index: i }))
|
|
2234
|
+
}
|
|
2235
|
+
if (!Object.hasOwn(res[i], 'type')) {
|
|
2236
|
+
throw new Error(RED._('clipboard.import.errors.missingType', { index: i }))
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return res
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1959
2242
|
function debug (...args) {
|
|
1960
2243
|
if (RED.nrAssistant?.DEBUG) {
|
|
1961
2244
|
const scriptName = 'assistant-index.html.js' // must match the sourceURL set in the script below
|
|
@@ -2010,7 +2293,7 @@
|
|
|
2010
2293
|
flex-grow: 1;
|
|
2011
2294
|
}
|
|
2012
2295
|
|
|
2013
|
-
/*
|
|
2296
|
+
/*
|
|
2014
2297
|
As menu icons are drawn by the (RED.menu.init api) as <img> tags, styling the fill of the path is impossible.
|
|
2015
2298
|
Instead of using a url (e.g. resource/icon.svg), we add them as a "class name" which node-red then renders as an `<i>` tag.
|
|
2016
2299
|
The CSS below uses the mask-image property to apply the SVG as a mask, allowing us to set the color via background-color.
|
|
@@ -2070,4 +2353,4 @@
|
|
|
2070
2353
|
.ff-nr-ai-dialog-message li {
|
|
2071
2354
|
margin-bottom: 3px;
|
|
2072
2355
|
}
|
|
2073
|
-
</style>
|
|
2356
|
+
</style>
|
package/lib/assistant.js
CHANGED
|
@@ -86,6 +86,7 @@ class Assistant {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
const clientSettings = {
|
|
89
|
+
assistantVersion: require('../package.json').version,
|
|
89
90
|
enabled: this.options.enabled !== false && !!this.options.url,
|
|
90
91
|
tablesEnabled: this.options.tables?.enabled === true,
|
|
91
92
|
inlineCompletionsEnabled: this.options.completions?.inlineEnabled === true,
|