@flowfuse/nr-assistant 0.1.1-cc561a2-202407081555.0 → 0.1.2-91d5229-202407160719.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/README.md CHANGED
@@ -25,7 +25,7 @@ The capabilities it adds to Node-RED can be found in Node-RED editor on the main
25
25
  {
26
26
  "flowforge": {
27
27
  "assistant": {
28
- "enabled": true
28
+ "enabled": true,
29
29
  "url": "https://", // URL of the AI service
30
30
  "token": "", // API token for the AI service
31
31
  "requestTimeout": 60000 // Timeout value for the AI service request
package/index.html CHANGED
@@ -1,6 +1,22 @@
1
1
  <script>
2
2
  (function (RED, n) {
3
3
  'use strict'
4
+
5
+ /**
6
+ * @typedef {Object} PromptOptions
7
+ * @property {string} method - The method used for routing the call in the API (e.g. 'function', 'json').
8
+ * @property {string} lang - The language type to be generated (e.g. 'javascript', 'json', 'yaml').
9
+ * @property {string} type - The type of the node.
10
+ * @property {string} [selectedText] - Any selected text. // future feature
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} PromptUIOptions
15
+ * @property {string} title - The title of the FlowFuse Assistant.
16
+ * @property {string} explanation - The explanation of what the FlowFuse Assistant can help with.
17
+ * @property {string} description - A short description of what you want the FlowFuse Assistant to do.
18
+ */
19
+
4
20
  const AI_TIMEOUT = 90000 // default request timeout in milliseconds
5
21
  const modulesAllowed = RED.settings.functionExternalModules !== false
6
22
  const assistantOptions = {
@@ -8,7 +24,7 @@
8
24
  requestTimeout: AI_TIMEOUT
9
25
  }
10
26
  let assistantInitialised = false
11
- debug('nr-assistant loading...')
27
+ debug('loading...')
12
28
  RED.plugins.registerPlugin('flowfuse-nr-assistant', {
13
29
  type: 'assistant',
14
30
  name: 'Node-RED Assistant Plugin',
@@ -29,7 +45,7 @@
29
45
  if (assistantInitialised) {
30
46
  return
31
47
  }
32
- debug('nr-assistant initialising...')
48
+ debug('initialising...')
33
49
  if (!assistantOptions.enabled) {
34
50
  console.warn('The FlowFuse Assistant is not enabled')
35
51
  return
@@ -39,7 +55,11 @@
39
55
  console.warn('Monaco editor not found. Unable to register code lens provider. Consider using the Monaco editor for a better experience.')
40
56
  return
41
57
  } else {
42
- const commandId = 'nr-assistant-fn-inline'
58
+ const funcCommandId = 'nr-assistant-fn-inline'
59
+ const jsonCommandId = 'nr-assistant-json-inline'
60
+
61
+ debug('registering code lens providers...')
62
+
43
63
  monaco.languages.registerCodeLensProvider('javascript', {
44
64
  provideCodeLenses: function (model, token) {
45
65
  const thisEditor = getMonacoEditorForModel(model)
@@ -77,14 +97,14 @@
77
97
  endLineNumber: 2,
78
98
  endColumn: 1
79
99
  },
80
- id: commandId
100
+ id: funcCommandId
81
101
  }
82
102
  ],
83
103
  dispose: () => { }
84
104
  }
85
105
  },
86
106
  resolveCodeLens: function (model, codeLens, token) {
87
- if (codeLens.id !== commandId) {
107
+ if (codeLens.id !== funcCommandId) {
88
108
  return codeLens
89
109
  }
90
110
  codeLens.command = {
@@ -97,8 +117,45 @@
97
117
  }
98
118
  })
99
119
 
100
- let previousPrompt = null
101
- monaco.editor.registerCommand(commandId, function (accessor, model, codeLens, token) {
120
+ monaco.languages.registerCodeLensProvider('json', {
121
+ provideCodeLenses: function (model, token) {
122
+ const thisEditor = getMonacoEditorForModel(model)
123
+ if (!thisEditor) {
124
+ return
125
+ }
126
+ return {
127
+ lenses: [
128
+ {
129
+ range: {
130
+ startLineNumber: 1,
131
+ startColumn: 1,
132
+ endLineNumber: 2,
133
+ endColumn: 1
134
+ },
135
+ id: jsonCommandId
136
+ }
137
+ ],
138
+ dispose: () => { }
139
+ }
140
+ },
141
+ resolveCodeLens: function (model, codeLens, token) {
142
+ if (codeLens.id !== jsonCommandId) {
143
+ return codeLens
144
+ }
145
+ codeLens.command = {
146
+ id: codeLens.id,
147
+ title: 'Ask the FlowFuse Assistant 🪄',
148
+ tooltip: 'Click to ask FlowFuse Assistant for help with JSON',
149
+ arguments: [model, codeLens, token]
150
+ }
151
+ return codeLens
152
+ }
153
+ })
154
+
155
+ debug('registering commands...')
156
+
157
+ monaco.editor.registerCommand(funcCommandId, function (accessor, model, codeLens, token) {
158
+ debug('running command', funcCommandId)
102
159
  const node = RED.view.selection()?.nodes?.[0]
103
160
  if (!node) {
104
161
  console.warn('No node selected') // should not happen
@@ -108,16 +165,15 @@
108
165
  RED.notify('The FlowFuse Assistant is not enabled', 'warning')
109
166
  return
110
167
  }
111
- let xhr = null
112
- const nodeId = node.id
113
- const transactionId = `${nodeId}-${Date.now()}` // a unique id for this transaction
114
168
  const thisEditor = getMonacoEditorForModel(model)
115
169
  if (thisEditor) {
116
170
  if (!document.body.contains(thisEditor.getDomNode())) {
117
171
  console.warn('Editor is no longer in the DOM, cannot proceed.')
118
172
  return
119
173
  }
174
+
120
175
  // walk up the tree to find the parent div with an id and include that in context
176
+ let subType = 'on message'
121
177
  let parent = thisEditor.getDomNode().parentNode
122
178
  while (parent?.tagName !== 'FORM') {
123
179
  if (parent.id) {
@@ -125,113 +181,163 @@
125
181
  }
126
182
  parent = parent.parentNode
127
183
  }
128
- // const editorSection = parent?.id || '' // FUTURE: determine the code section users code lens was triggered in. Will be used to help the assistant understand the context of the code
129
- const editorSection = 'on message' // hard coded for now since the code lens is only available in the "on message" editor in this first iteration
184
+ switch (parent?.id) {
185
+ case 'func-tab-init':
186
+ case 'node-input-init-editor':
187
+ subType = 'on start'
188
+ break
189
+ case 'func-tab-body':
190
+ case 'node-input-func-editor':
191
+ subType = 'on message'
192
+ break
193
+ case 'func-tab-finalize':
194
+ case 'node-input-finalize-editor':
195
+ subType = 'on message'
196
+ break
197
+ }
130
198
 
131
199
  // FUTURE: for including selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
132
200
  // const userSelection = triggeredEditor.getSelection()
133
201
  // const selectedText = model.getValueInRange(userSelection)
134
-
135
- getUserInput({
136
- defaultInput: previousPrompt,
202
+ /** @type {PromptOptions} */
203
+ const promptOptions = {
204
+ method: 'function',
205
+ lang: 'javascript',
206
+ type: 'function',
207
+ subType
208
+ // selectedText: model.getValueInRange(userSelection)
209
+ }
210
+ /** @type {PromptUIOptions} */
211
+ const uiOptions = {
137
212
  title: 'FlowFuse Assistant : Function Code',
138
- explanation: 'The FlowFuse Assistant can help you write code.',
139
- description: 'Enter a short description of what you want the function to do.'
140
- }).then((prompt) => {
141
- if (prompt) {
142
- previousPrompt = prompt
143
- const data = {
144
- prompt,
145
- transactionId,
146
- context: {
147
- scope: 'inline', // inline denotes that the prompt is for a inline code (i.e. the monaco editor)
148
- modulesAllowed,
149
- codeSection: editorSection
150
- // selection: selectedText // FUTURE: include the selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
151
- }
213
+ explanation: 'The FlowFuse Assistant can help you write JavaScript code.',
214
+ description: 'Enter a short description of what you want it to do.'
215
+ }
216
+ doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
217
+ if (error) {
218
+ console.warn('Error processing request', error)
219
+ return
220
+ }
221
+ debug('function response', response)
222
+ const responseData = response?.data
223
+ if (responseData?.func?.length > 0) {
224
+ // ensure the editor is still present in the DOM
225
+ if (!document.body.contains(thisEditor.getDomNode())) {
226
+ console.warn('Editor is no longer in the DOM')
227
+ return
152
228
  }
153
- const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
154
- if (xhr) {
155
- xhr.abort('abort')
156
- xhr = null
229
+ thisEditor.focus()
230
+ // insert the generated code at the current cursor position overwriting any selected text
231
+ const currentSelection = thisEditor.getSelection()
232
+ thisEditor.executeEdits('', [
233
+ {
234
+ range: new monaco.Range(currentSelection.startLineNumber, currentSelection.startColumn, currentSelection.endLineNumber, currentSelection.endColumn),
235
+ text: responseData.func
157
236
  }
158
- })
159
- xhr = $.ajax({
160
- url: 'nr-assistant/function', // /function denotes this is a request for NODE JavaScript code
161
- type: 'POST',
162
- data,
163
- success: function (reply, textStatus, jqXHR) {
164
- const responseData = reply?.data?.data
165
- if (responseData?.func?.length > 0) {
166
- const currentSelection = thisEditor.getSelection()
167
- // ensure the editor is still present in the DOM
168
- if (!document.body.contains(thisEditor.getDomNode())) {
169
- console.warn('Editor is no longer in the DOM')
170
- return
237
+ ])
238
+ // update the nodes output count the AI suggests a different number of outputs
239
+ if (typeof responseData?.outputs === 'number' && responseData.outputs >= 0) {
240
+ const outputsField = $('#node-input-outputs')
241
+ const currentOutputs = parseInt(outputsField.val())
242
+ if (!isNaN(currentOutputs) && typeof currentOutputs === 'number' && currentOutputs !== responseData.outputs) {
243
+ outputsField.val(responseData.outputs)
244
+ outputsField.trigger('change')
245
+ }
246
+ }
247
+
248
+ // update libs - get the current list of libs then scan the response for any new ones
249
+ // if the lib is not already in the list, add it
250
+ if (modulesAllowed) {
251
+ if (Array.isArray(responseData?.node_modules) && responseData.node_modules.length > 0) {
252
+ const _libs = []
253
+ const libs = $('#node-input-libs-container').editableList('items')
254
+ libs.each(function (i) {
255
+ const item = $(this)
256
+ const v = item.find('.node-input-libs-var').val()
257
+ let n = item.find('.node-input-libs-val').typedInput('type')
258
+ if (n === '_custom_') {
259
+ n = item.find('.node-input-libs-val').val()
171
260
  }
172
- busyNotification.close()
173
- thisEditor.focus()
174
- // insert the generated code at the current cursor position overwriting any selected text
175
- thisEditor.executeEdits('', [
176
- {
177
- range: new monaco.Range(currentSelection.startLineNumber, currentSelection.startColumn, currentSelection.endLineNumber, currentSelection.endColumn),
178
- text: responseData.func
179
- }
180
- ])
181
- // update the nodes output count the AI suggests a different number of outputs
182
- if (typeof responseData?.outputs === 'number' && responseData.outputs >= 0) {
183
- const outputsField = $('#node-input-outputs')
184
- const currentOutputs = parseInt(outputsField.val())
185
- if (!isNaN(currentOutputs) && typeof currentOutputs === 'number' && currentOutputs !== responseData.outputs) {
186
- outputsField.val(responseData.outputs)
187
- outputsField.trigger('change')
188
- }
261
+ if ((!v || (v === '')) ||
262
+ (!n || (n === ''))) {
263
+ return
189
264
  }
265
+ _libs.push({
266
+ var: v,
267
+ module: n
268
+ })
269
+ })
190
270
 
191
- // update libs - get the current list of libs then scan the response for any new ones
192
- // if the lib is not already in the list, add it
193
- if (modulesAllowed) {
194
- if (Array.isArray(responseData?.node_modules) && responseData.node_modules.length > 0) {
195
- const _libs = []
196
- const libs = $('#node-input-libs-container').editableList('items')
197
- libs.each(function (i) {
198
- const item = $(this)
199
- const v = item.find('.node-input-libs-var').val()
200
- let n = item.find('.node-input-libs-val').typedInput('type')
201
- if (n === '_custom_') {
202
- n = item.find('.node-input-libs-val').val()
203
- }
204
- if ((!v || (v === '')) ||
205
- (!n || (n === ''))) {
206
- return
207
- }
208
- _libs.push({
209
- var: v,
210
- module: n
211
- })
212
- })
213
-
214
- responseData.node_modules.forEach((lib) => {
215
- const existing = _libs.find(l => l.module === lib.module)
216
- if (!existing) {
217
- $('#node-input-libs-container').editableList('addItem', { var: lib.var, module: lib.module })
218
- }
219
- })
220
- }
271
+ responseData.node_modules.forEach((lib) => {
272
+ const existing = _libs.find(l => l.module === lib.module)
273
+ if (!existing) {
274
+ $('#node-input-libs-container').editableList('addItem', { var: lib.var, module: lib.module })
221
275
  }
222
- } else {
223
- processAIErrorResponse(jqXHR, textStatus, 'No response from server')
224
- }
225
- },
226
- error: (jqXHR, textStatus, errorThrown) => {
227
- busyNotification.close()
228
- if (textStatus === 'abort' || errorThrown === 'abort' || jqXHR.statusText === 'abort') {
229
- // user cancelled
230
- return
231
- }
232
- processAIErrorResponse(jqXHR, textStatus, errorThrown)
276
+ })
277
+ }
278
+ }
279
+ }
280
+ })
281
+ } else {
282
+ console.warn('Could not find editor for model', model.uri.toString())
283
+ }
284
+ })
285
+
286
+ monaco.editor.registerCommand(jsonCommandId, function (accessor, model, codeLens, token) {
287
+ debug('running command', jsonCommandId)
288
+ const node = RED.view.selection()?.nodes?.[0]
289
+ if (!node) {
290
+ console.warn('No node selected') // should not happen
291
+ return
292
+ }
293
+ if (!assistantOptions.enabled) {
294
+ RED.notify('The FlowFuse Assistant is not enabled', 'warning')
295
+ return
296
+ }
297
+ const thisEditor = getMonacoEditorForModel(model)
298
+ if (thisEditor) {
299
+ if (!document.body.contains(thisEditor.getDomNode())) {
300
+ console.warn('Editor is no longer in the DOM, cannot proceed.')
301
+ return
302
+ }
303
+
304
+ // FUTURE: for including selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
305
+ // const userSelection = triggeredEditor.getSelection()
306
+ // const selectedText = model.getValueInRange(userSelection)
307
+ /** @type {PromptOptions} */
308
+ const promptOptions = {
309
+ method: 'json',
310
+ lang: 'json',
311
+ type: node.type
312
+ // selectedText: model.getValueInRange(userSelection)
313
+ }
314
+ /** @type {PromptUIOptions} */
315
+ const uiOptions = {
316
+ title: 'FlowFuse Assistant : JSON',
317
+ explanation: 'The FlowFuse Assistant can help you write JSON.',
318
+ description: 'Enter a short description of what you want it to do.'
319
+ }
320
+ doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
321
+ if (error) {
322
+ console.warn('Error processing request', error)
323
+ return
324
+ }
325
+ debug('json response', response)
326
+ const responseData = response?.data
327
+ if (responseData && responseData.json) {
328
+ // ensure the editor is still present in the DOM
329
+ if (!document.body.contains(thisEditor.getDomNode())) {
330
+ console.warn('Editor is no longer in the DOM')
331
+ return
332
+ }
333
+ thisEditor.focus()
334
+ const currentSelection = thisEditor.getSelection()
335
+ thisEditor.executeEdits('', [
336
+ {
337
+ range: new monaco.Range(currentSelection.startLineNumber, currentSelection.startColumn, currentSelection.endLineNumber, currentSelection.endColumn),
338
+ text: responseData.json
233
339
  }
234
- })
340
+ ])
235
341
  }
236
342
  })
237
343
  } else {
@@ -248,16 +354,114 @@
248
354
  // const flowBuilderTitle = 'FlowFuse Flow Assistant'
249
355
  // RED.actions.add('ff:nr-assistant-flow-builder', showFlowBuilderPrompt, { label: flowBuilderTitle })
250
356
 
251
- // Add toolbar button
252
- const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button fa fa-magic" href="#"></a></li>')
253
- toolbarMenuButton.prependTo('.red-ui-header-toolbar')
254
- toolbarMenuButton.on('click', function (e) {
357
+ const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button" href="#"></a></li>')
358
+ const toolbarMenuButtonAnchor = toolbarMenuButton.find('a')
359
+ toolbarMenuButtonAnchor.css('mask-image', 'url("/resources/@flowfuse/nr-assistant/assistant-button.svg")')
360
+ toolbarMenuButtonAnchor.css('mask-repeat', 'no-repeat')
361
+ toolbarMenuButtonAnchor.css('mask-position', 'center')
362
+ toolbarMenuButtonAnchor.css('background-color', 'currentColor')
363
+ const deployButtonLi = $('#red-ui-header-button-deploy').closest('li')
364
+ if (deployButtonLi.length) {
365
+ deployButtonLi.before(toolbarMenuButton) // add the button before the deploy button
366
+ } else {
367
+ toolbarMenuButton.prependTo('.red-ui-header-toolbar') // add the button leftmost of the toolbar
368
+ }
369
+ toolbarMenuButtonAnchor.on('click', function (e) {
255
370
  RED.actions.invoke('ff:nr-assistant-function-builder')
256
371
  })
257
- RED.popover.tooltip(toolbarMenuButton, 'FlowFuse Assistant')
372
+ RED.popover.tooltip(toolbarMenuButtonAnchor, 'FlowFuse Assistant')
258
373
  assistantInitialised = true
259
374
  }
260
375
 
376
+ const previousPrompts = {}
377
+
378
+ /**
379
+ * Prompts the user for input and sends the input to the AI for processing
380
+ * @param {import('node-red').Node} node - The node to associate the prompt with
381
+ * @param {import('monaco-editor').editor.IStandaloneCodeEditor} editor - The editor to associate the prompt with
382
+ * @param {PromptOptions} promptOptions - The options to pass to the prompt
383
+ * @param {PromptUIOptions} [uiOptions] - The options to pass to the prompt
384
+ * @param {(error, response) => {}} callback - The callback function to call when the prompt is complete
385
+ * @returns {void}
386
+ */
387
+ function doPrompt (node, editor, promptOptions, uiOptions, callback) {
388
+ const thisEditor = editor
389
+ let xhr = null
390
+ if (!node || !thisEditor) {
391
+ console.warn('No node or editor found')
392
+ callback(null, null)
393
+ }
394
+ const modulesAllowed = RED.settings.functionExternalModules !== false
395
+ promptOptions = promptOptions || {}
396
+
397
+ const nodeId = node.id
398
+ const transactionId = `${nodeId}-${Date.now()}` // a unique id for this transaction
399
+ const prevPromptKey = `${promptOptions?.method}-${promptOptions?.subType || 'default'}`
400
+ const defaultInput = Object.prototype.hasOwnProperty.call(uiOptions, 'defaultInput') ? uiOptions.defaultInput : previousPrompts[prevPromptKey] || ''
401
+ debug('doPrompt', promptOptions, uiOptions)
402
+ getUserInput({
403
+ defaultInput: defaultInput || '',
404
+ title: uiOptions?.title || 'FlowFuse Assistant',
405
+ explanation: uiOptions?.explanation || 'The FlowFuse Assistant can help you write code.',
406
+ description: uiOptions?.description || 'Enter a short description of what you want it to do.'
407
+ }).then((prompt) => {
408
+ if (!prompt) {
409
+ callback(null, null)
410
+ }
411
+ previousPrompts[prevPromptKey] = prompt
412
+ const data = {
413
+ prompt,
414
+ transactionId,
415
+ context: {
416
+ type: promptOptions.type,
417
+ subType: promptOptions.subType,
418
+ scope: 'inline', // inline denotes that the prompt is for a inline code (i.e. the monaco editor)
419
+ modulesAllowed,
420
+ codeSection: promptOptions.subType
421
+ // selection: selectedText // FUTURE: include the selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
422
+ }
423
+ }
424
+ const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
425
+ if (xhr) {
426
+ xhr.abort('abort')
427
+ xhr = null
428
+ }
429
+ })
430
+ xhr = $.ajax({
431
+ url: 'nr-assistant/' + (promptOptions.method || promptOptions.lang), // e.g. 'nr-assistant/json'
432
+ type: 'POST',
433
+ data,
434
+ success: function (reply, textStatus, jqXHR) {
435
+ debug('doPrompt -> ajax -> success', reply)
436
+ if (reply?.error) {
437
+ RED.notify(reply.error, 'error')
438
+ callback(new Error(reply.error), null)
439
+ return
440
+ }
441
+ if (reply?.data?.transactionId !== transactionId) {
442
+ callback(new Error('Transaction ID mismatch'), null)
443
+ return
444
+ }
445
+ callback(null, reply?.data)
446
+ },
447
+ error: (jqXHR, textStatus, errorThrown) => {
448
+ debug('doPrompt -> ajax -> error', jqXHR, textStatus, errorThrown)
449
+ if (textStatus === 'abort' || errorThrown === 'abort' || jqXHR.statusText === 'abort') {
450
+ // user cancelled
451
+ callback(null, null)
452
+ return
453
+ }
454
+ processAIErrorResponse(jqXHR, textStatus, errorThrown)
455
+ callback(new Error('Error processing request'), null)
456
+ },
457
+ complete: function () {
458
+ xhr = null
459
+ busyNotification.close()
460
+ }
461
+ })
462
+ })
463
+ }
464
+
261
465
  function getUserInput ({ title, explanation, description, placeholder, defaultInput } = {
262
466
  title: 'FlowFuse Assistant',
263
467
  explanation: 'The FlowFuse Assistant can help you create things.',
@@ -369,7 +573,7 @@
369
573
  if (flowJson && Array.isArray(flowJson) && flowJson.length > 0) {
370
574
  importFlow(flowJson)
371
575
  } else {
372
- processAIErrorResponse(jqXHR, textStatus, 'No response from server')
576
+ processAIErrorResponse(jqXHR, textStatus, 'No data in response from server')
373
577
  }
374
578
  } catch (error) {
375
579
  RED.notify('Sorry, something went wrong, please try again', 'error')
@@ -433,7 +637,7 @@
433
637
  if (flowJson && Array.isArray(flowJson) && flowJson.length > 0) {
434
638
  importFlow(flowJson)
435
639
  } else {
436
- processAIErrorResponse(jqXHR, textStatus, 'No response from server')
640
+ processAIErrorResponse(jqXHR, textStatus, 'No data in response from server')
437
641
  }
438
642
  } catch (error) {
439
643
  RED.notify('Sorry, something went wrong, please try again', 'error')
@@ -506,6 +710,9 @@
506
710
  }
507
711
 
508
712
  function importFlow (flow, addFlow) {
713
+ if (RED.workspaces.isLocked && RED.workspaces.isLocked()) {
714
+ addFlow = true // force import to create a new tab
715
+ }
509
716
  let newNodes = flow
510
717
  try {
511
718
  if (typeof flow === 'string') {
@@ -560,4 +767,4 @@
560
767
  }
561
768
  }
562
769
  }(RED, $))
563
- </script>
770
+ </script>
package/index.js CHANGED
@@ -27,22 +27,21 @@ module.exports = (RED) => {
27
27
  RED.log.info('FlowFuse Assistant Plugin loaded')
28
28
 
29
29
  RED.httpAdmin.post('/nr-assistant/:method', RED.auth.needsPermission('write'), function (req, res) {
30
- const method = req.params.method || 'fn'
30
+ const method = req.params.method
31
31
  // limit method to prevent path traversal
32
- if (/[^a-zA-Z0-9-_]/.test(method)) {
32
+ if (!method || typeof method !== 'string' || /[^a-z0-9-_]/.test(method)) {
33
33
  res.status(400)
34
34
  res.json({ status: 'error', message: 'Invalid method' })
35
35
  return
36
36
  }
37
37
  const input = req.body
38
- const prompt = input.prompt
39
- if (!input.prompt) {
38
+ if (!input || !input.prompt || typeof input.prompt !== 'string') {
40
39
  res.status(400)
41
40
  res.json({ status: 'error', message: 'prompt is required' })
42
41
  return
43
42
  }
44
43
  const body = {
45
- prompt, // this is the prompt to the AI
44
+ prompt: input.prompt, // this is the prompt to the AI
46
45
  promptHint: input.promptHint, // this is used to let the AI know what we are generating (`function node? Node JavaScript? flow?)
47
46
  context: input.context, // this is used to provide additional context to the AI (e.g. the selected text of the function node)
48
47
  transactionId: input.transactionId // used to correlate the request with the response
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/nr-assistant",
3
- "version": "0.1.1-cc561a2-202407081555.0",
3
+ "version": "0.1.2-91d5229-202407160719.0",
4
4
  "description": "FlowFuse Node-RED assistant plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,5 @@
1
+ <svg width="30" height="30" viewBox="0 0 30 30" style="" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M26.9949 12.5515C26.5792 12.5515 26.2437 12.8871 26.2437 13.3028V19.6387H19.6674C18.4903 19.6437 17.4836 20.4751 17.3734 21.6321C16.5019 21.5219 15.971 21.2815 15.5903 20.9309C15.1396 20.5202 14.834 19.9342 14.4584 19.253C14.0828 18.5719 13.622 17.7905 12.7805 17.2145C12.1044 16.7487 11.1026 16.5183 9.93062 16.4582V16.0525C9.93062 14.7954 8.95394 13.7836 7.69678 13.7836H1.81166V4.82323C1.81166 4.12703 2.37764 3.55605 3.07884 3.55605H16.7624C17.1781 3.55605 17.5137 3.22047 17.5137 2.80476C17.5137 2.38904 17.1781 2.05347 16.7624 2.05347H3.07884C1.55122 2.05347 0.309082 3.2956 0.309082 4.82323V26.7209C0.309082 28.2485 1.55122 29.4906 3.07884 29.4906H24.9815C26.5091 29.4906 27.7512 28.2485 27.7512 26.7209V13.2978C27.7512 12.8821 27.4157 12.5465 27 12.5465L26.9949 12.5515ZM18.9311 21.8425C18.9311 21.4167 19.2466 21.0661 19.6724 21.0661H26.2437V24.7775H19.6724C19.2466 24.7775 18.9311 24.497 18.9311 24.0713V21.8425ZM7.69678 15.3563C8.12251 15.3563 8.46811 15.6268 8.46811 16.0525V18.2864C8.46811 18.7071 8.12251 18.9976 7.70179 18.9976H1.81166V15.3563H7.69678ZM24.9765 27.988H3.07884C2.38265 27.988 1.81166 27.4221 1.81166 26.7209V20.5603H7.70179C8.95394 20.5603 9.93062 19.5335 9.93062 18.2814V17.9909C10.8171 18.0409 11.5033 18.2062 11.904 18.4817C12.4199 18.8373 12.7455 19.3482 13.1011 19.9943C13.4316 20.5953 13.8023 21.3016 14.4233 21.9226H14.4283C14.4885 21.9977 14.9893 22.5437 15.6505 22.8542C16.2765 23.1547 17.3283 23.2198 17.3283 23.2198V24.0663C17.3283 25.3235 18.4152 26.3402 19.6674 26.3402H26.2437V26.7209C26.2437 27.4171 25.6777 27.988 24.9765 27.988Z" fill="var(--red-ui-header-menu-color)"></path>
3
+ <path d="M29.5043 4.97859L27.1402 4.10208C26.4691 3.85666 25.9432 3.32575 25.6978 2.6596L24.8213 0.295539C24.676 -0.100141 24.115 -0.100141 23.9698 0.295539L23.0933 2.6596C22.8479 3.33076 22.317 3.85666 21.6508 4.10208L19.2868 4.97859C18.8911 5.12384 18.8911 5.6848 19.2868 5.83005L21.6508 6.70656C22.322 6.95198 22.8479 7.48289 23.0933 8.14904L23.9698 10.5131C24.115 10.9088 24.676 10.9088 24.8213 10.5131L25.6978 8.14904C25.9432 7.47788 26.4741 6.95198 27.1402 6.70656L29.5043 5.83005C29.9 5.6848 29.9 5.12384 29.5043 4.97859Z" style="fill: var(--red-ui-header-menu-color);"></path>
4
+ <path d="M16.6822 11.2944L17.8643 11.7302C18.1998 11.8554 18.4603 12.1158 18.5855 12.4514L19.0213 13.6334C19.0964 13.8338 19.3719 13.8338 19.447 13.6334L19.8827 12.4514C20.008 12.1158 20.2684 11.8554 20.604 11.7302L21.786 11.2944C21.9864 11.2193 21.9864 10.9438 21.786 10.8687L20.604 10.4329C20.2684 10.3077 20.008 10.0473 19.8827 9.71168L19.447 8.52965C19.3719 8.32931 19.0914 8.32931 19.0213 8.52965L18.5855 9.71168C18.4603 10.0473 18.1998 10.3077 17.8643 10.4329L16.6822 10.8687C16.4819 10.9438 16.4819 11.2193 16.6822 11.2944Z" style="fill: var(--red-ui-header-menu-color);"></path>
5
+ </svg>