@flowfuse/nr-assistant 0.1.1-cc561a2-202407081555.0 → 0.1.2-72f7066-202407151302.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 +306 -108
- package/index.js +4 -5
- package/package.json +1 -1
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('
|
|
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('
|
|
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
|
|
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:
|
|
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 !==
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
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
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 {
|
|
@@ -258,6 +364,95 @@
|
|
|
258
364
|
assistantInitialised = true
|
|
259
365
|
}
|
|
260
366
|
|
|
367
|
+
const previousPrompts = {}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Prompts the user for input and sends the input to the AI for processing
|
|
371
|
+
* @param {import('node-red').Node} node - The node to associate the prompt with
|
|
372
|
+
* @param {import('monaco-editor').editor.IStandaloneCodeEditor} editor - The editor to associate the prompt with
|
|
373
|
+
* @param {PromptOptions} promptOptions - The options to pass to the prompt
|
|
374
|
+
* @param {PromptUIOptions} [uiOptions] - The options to pass to the prompt
|
|
375
|
+
* @param {(error, response) => {}} callback - The callback function to call when the prompt is complete
|
|
376
|
+
* @returns {void}
|
|
377
|
+
*/
|
|
378
|
+
function doPrompt (node, editor, promptOptions, uiOptions, callback) {
|
|
379
|
+
const thisEditor = editor
|
|
380
|
+
let xhr = null
|
|
381
|
+
if (!node || !thisEditor) {
|
|
382
|
+
console.warn('No node or editor found')
|
|
383
|
+
callback(null, null)
|
|
384
|
+
}
|
|
385
|
+
const modulesAllowed = RED.settings.functionExternalModules !== false
|
|
386
|
+
promptOptions = promptOptions || {}
|
|
387
|
+
|
|
388
|
+
const nodeId = node.id
|
|
389
|
+
const transactionId = `${nodeId}-${Date.now()}` // a unique id for this transaction
|
|
390
|
+
const prevPromptKey = `${promptOptions?.method}-${promptOptions?.subType || 'default'}`
|
|
391
|
+
const defaultInput = Object.prototype.hasOwnProperty.call(uiOptions, 'defaultInput') ? uiOptions.defaultInput : previousPrompts[prevPromptKey] || ''
|
|
392
|
+
debug('doPrompt', promptOptions, uiOptions)
|
|
393
|
+
getUserInput({
|
|
394
|
+
defaultInput: defaultInput || '',
|
|
395
|
+
title: uiOptions?.title || 'FlowFuse Assistant',
|
|
396
|
+
explanation: uiOptions?.explanation || 'The FlowFuse Assistant can help you write code.',
|
|
397
|
+
description: uiOptions?.description || 'Enter a short description of what you want it to do.'
|
|
398
|
+
}).then((prompt) => {
|
|
399
|
+
if (!prompt) {
|
|
400
|
+
callback(null, null)
|
|
401
|
+
}
|
|
402
|
+
previousPrompts[prevPromptKey] = prompt
|
|
403
|
+
const data = {
|
|
404
|
+
prompt,
|
|
405
|
+
transactionId,
|
|
406
|
+
context: {
|
|
407
|
+
type: promptOptions.type,
|
|
408
|
+
subType: promptOptions.subType,
|
|
409
|
+
scope: 'inline', // inline denotes that the prompt is for a inline code (i.e. the monaco editor)
|
|
410
|
+
modulesAllowed,
|
|
411
|
+
codeSection: promptOptions.subType
|
|
412
|
+
// selection: selectedText // FUTURE: include the selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
|
|
416
|
+
if (xhr) {
|
|
417
|
+
xhr.abort('abort')
|
|
418
|
+
xhr = null
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
xhr = $.ajax({
|
|
422
|
+
url: 'nr-assistant/' + (promptOptions.method || promptOptions.lang), // e.g. 'nr-assistant/json'
|
|
423
|
+
type: 'POST',
|
|
424
|
+
data,
|
|
425
|
+
success: function (reply, textStatus, jqXHR) {
|
|
426
|
+
debug('doPrompt -> ajax -> success', reply)
|
|
427
|
+
if (reply?.error) {
|
|
428
|
+
RED.notify(reply.error, 'error')
|
|
429
|
+
callback(new Error(reply.error), null)
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
if (reply?.data?.transactionId !== transactionId) {
|
|
433
|
+
callback(new Error('Transaction ID mismatch'), null)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
callback(null, reply?.data)
|
|
437
|
+
},
|
|
438
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
439
|
+
debug('doPrompt -> ajax -> error', jqXHR, textStatus, errorThrown)
|
|
440
|
+
if (textStatus === 'abort' || errorThrown === 'abort' || jqXHR.statusText === 'abort') {
|
|
441
|
+
// user cancelled
|
|
442
|
+
callback(null, null)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
processAIErrorResponse(jqXHR, textStatus, errorThrown)
|
|
446
|
+
callback(new Error('Error processing request'), null)
|
|
447
|
+
},
|
|
448
|
+
complete: function () {
|
|
449
|
+
xhr = null
|
|
450
|
+
busyNotification.close()
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
261
456
|
function getUserInput ({ title, explanation, description, placeholder, defaultInput } = {
|
|
262
457
|
title: 'FlowFuse Assistant',
|
|
263
458
|
explanation: 'The FlowFuse Assistant can help you create things.',
|
|
@@ -369,7 +564,7 @@
|
|
|
369
564
|
if (flowJson && Array.isArray(flowJson) && flowJson.length > 0) {
|
|
370
565
|
importFlow(flowJson)
|
|
371
566
|
} else {
|
|
372
|
-
processAIErrorResponse(jqXHR, textStatus, 'No response from server')
|
|
567
|
+
processAIErrorResponse(jqXHR, textStatus, 'No data in response from server')
|
|
373
568
|
}
|
|
374
569
|
} catch (error) {
|
|
375
570
|
RED.notify('Sorry, something went wrong, please try again', 'error')
|
|
@@ -433,7 +628,7 @@
|
|
|
433
628
|
if (flowJson && Array.isArray(flowJson) && flowJson.length > 0) {
|
|
434
629
|
importFlow(flowJson)
|
|
435
630
|
} else {
|
|
436
|
-
processAIErrorResponse(jqXHR, textStatus, 'No response from server')
|
|
631
|
+
processAIErrorResponse(jqXHR, textStatus, 'No data in response from server')
|
|
437
632
|
}
|
|
438
633
|
} catch (error) {
|
|
439
634
|
RED.notify('Sorry, something went wrong, please try again', 'error')
|
|
@@ -506,6 +701,9 @@
|
|
|
506
701
|
}
|
|
507
702
|
|
|
508
703
|
function importFlow (flow, addFlow) {
|
|
704
|
+
if (RED.workspaces.isLocked && RED.workspaces.isLocked()) {
|
|
705
|
+
addFlow = true // force import to create a new tab
|
|
706
|
+
}
|
|
509
707
|
let newNodes = flow
|
|
510
708
|
try {
|
|
511
709
|
if (typeof flow === 'string') {
|
|
@@ -560,4 +758,4 @@
|
|
|
560
758
|
}
|
|
561
759
|
}
|
|
562
760
|
}(RED, $))
|
|
563
|
-
</script>
|
|
761
|
+
</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
|
|
30
|
+
const method = req.params.method
|
|
31
31
|
// limit method to prevent path traversal
|
|
32
|
-
if (/[^a-
|
|
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
|
-
|
|
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
|