@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 +1 -1
- package/index.html +320 -113
- package/index.js +4 -5
- package/package.json +1 -1
- package/resources/assistant-button.svg +5 -0
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('
|
|
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 {
|
|
@@ -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
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
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(
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -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>
|