@flowfuse/nr-assistant 0.6.1-f938e50-202512021006.0 → 0.6.1-fd37d56-202512091447.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 +4 -27
- package/completions.html +1 -1
- package/index.html +166 -84
- package/index.js +19 -11
- package/lib/assistant.js +83 -43
- package/lib/auth/index.js +277 -0
- package/lib/auth/store.js +33 -0
- package/lib/settings.js +57 -20
- package/locales/en-US/index.json +14 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# FlowFuse Node-RED
|
|
1
|
+
# FlowFuse Node-RED Expert Plugin
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A plugin to bring AI assistance to your Node-RED flow building.
|
|
4
4
|
|
|
5
|
+
This plugin is preinstalled in Node-RED instances hosted and managed by the FlowFuse platform.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
It can also be installed locally for use outside of FlowFuse - but does require a FlowFuse Cloud user account.
|
|
7
8
|
|
|
8
9
|
FlowFuse is the Industrial application platform for building and operating custom industrial solutions that digitalize processes and operations. It integrates seamlessly into both IT and OT environments, leveraging Node-RED to enable teams to connect, collect, transform, and visualize data from industrial systems. Companies use FlowFuse to manage, scale, and secure their Node-RED-based applications across industrial environments.
|
|
9
10
|
|
|
@@ -36,7 +37,6 @@ This plugin is designed to assist users of the FlowFuse platform by providing to
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
### Inline Code Completions
|
|
39
|
-
NOTE: This feature will be limited to pro and enterprise tiers.
|
|
40
40
|
|
|
41
41
|
#### functions
|
|
42
42
|

|
|
@@ -57,29 +57,6 @@ npm install @flowfuse/nr-assistant
|
|
|
57
57
|
|
|
58
58
|
Client-side portion of the plugin is in `index.html`. The server side code is in `index.js`
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
## NOTES:
|
|
62
|
-
|
|
63
|
-
* Requires the settings.js file to contain the following:
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
{
|
|
67
|
-
"flowforge": {
|
|
68
|
-
"assistant": {
|
|
69
|
-
"enabled": true,
|
|
70
|
-
"url": "https://", // URL of the AI service
|
|
71
|
-
"token": "", // API token for the AI service
|
|
72
|
-
"requestTimeout": 60000 // Timeout value for the AI service request
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
These values are automatically set when running within the FlowFuse platform via the `nr-launcher` component.
|
|
79
|
-
|
|
80
|
-
The `url` and `token` are for an AI service hosted by FlowFuse; it is not publicly available for use outside of the FlowFuse platform.
|
|
81
|
-
|
|
82
|
-
|
|
83
60
|
## Limitations
|
|
84
61
|
|
|
85
62
|
### Function Builder
|
package/completions.html
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
debug('prediction', typeSearchSuggestions)
|
|
92
92
|
return typeSearchSuggestions || []
|
|
93
93
|
},
|
|
94
|
-
name: 'Node-RED
|
|
94
|
+
name: 'Node-RED Expert Completions',
|
|
95
95
|
icon: 'font-awesome/fa-magic',
|
|
96
96
|
onadd: async function () {
|
|
97
97
|
if (!window.FFAssistantUtils) {
|
package/index.html
CHANGED
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
let initialisedInterlock = false
|
|
51
51
|
let mcpReadyInterlock = false
|
|
52
52
|
let assistantInitialised = false
|
|
53
|
-
let mcpReady = false
|
|
54
53
|
debug('Loading Node-RED Assistant Plugin...')
|
|
55
54
|
const plugin = {
|
|
56
55
|
type: 'assistant',
|
|
@@ -62,20 +61,18 @@
|
|
|
62
61
|
}
|
|
63
62
|
RED.comms.subscribe('nr-assistant/#', (topic, msg) => {
|
|
64
63
|
debug('comms', topic, msg)
|
|
65
|
-
if (topic === 'nr-assistant/initialise'
|
|
66
|
-
|
|
64
|
+
if (topic === 'nr-assistant/initialise') {
|
|
65
|
+
assistantOptions.standalone = !!msg?.standalone
|
|
67
66
|
assistantOptions.enabled = !!msg?.enabled
|
|
68
67
|
assistantOptions.requestTimeout = msg?.requestTimeout || AI_TIMEOUT
|
|
69
68
|
assistantOptions.tablesEnabled = msg?.tablesEnabled === true
|
|
70
69
|
assistantOptions.inlineCompletionsEnabled = msg?.inlineCompletionsEnabled === true
|
|
71
70
|
initAssistant(msg)
|
|
72
|
-
RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:function-builder.action.label' })
|
|
73
|
-
setMenuShortcutKey('ff-assistant-function-builder', 'red-ui-workspace', 'ctrl-alt-f', 'flowfuse-nr-assistant:function-builder')
|
|
74
71
|
}
|
|
75
|
-
if (topic === 'nr-assistant/mcp/ready'
|
|
76
|
-
mcpReadyInterlock
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
if (topic === 'nr-assistant/mcp/ready') {
|
|
73
|
+
if (!mcpReadyInterlock && !!msg?.enabled) {
|
|
74
|
+
mcpReadyInterlock = true
|
|
75
|
+
// Complete first time setup
|
|
79
76
|
debug('assistant MCP initialised')
|
|
80
77
|
RED.actions.add('flowfuse-nr-assistant:explain-flows', explainSelectedNodes, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:explain-flows.action.label' })
|
|
81
78
|
const menuEntry = {
|
|
@@ -89,6 +86,12 @@
|
|
|
89
86
|
}
|
|
90
87
|
RED.menu.addItem('red-ui-header-button-ff-ai', menuEntry)
|
|
91
88
|
setMenuShortcutKey('ff-assistant-explain-flows', 'red-ui-workspace', 'ctrl-alt-e', 'flowfuse-nr-assistant:explain-flows')
|
|
89
|
+
} else if (mcpReadyInterlock) {
|
|
90
|
+
if (msg?.enabled) {
|
|
91
|
+
RED.menu.setVisible('ff-assistant-explain-flows', true)
|
|
92
|
+
} else {
|
|
93
|
+
RED.menu.setVisible('ff-assistant-explain-flows', false)
|
|
94
|
+
}
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
})
|
|
@@ -96,21 +99,117 @@
|
|
|
96
99
|
}
|
|
97
100
|
RED.plugins.registerPlugin('flowfuse-nr-assistant', plugin)
|
|
98
101
|
|
|
102
|
+
function createAssistantMenu () {
|
|
103
|
+
const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button" href="#"></a></li>')
|
|
104
|
+
const toolbarMenuButtonAnchor = toolbarMenuButton.find('a')
|
|
105
|
+
const deployButtonLi = $('#red-ui-header-button-deploy').closest('li')
|
|
106
|
+
if (deployButtonLi.length) {
|
|
107
|
+
deployButtonLi.before(toolbarMenuButton) // add the button before the deploy button
|
|
108
|
+
} else {
|
|
109
|
+
toolbarMenuButton.prependTo('.red-ui-header-toolbar') // add the button leftmost of the toolbar
|
|
110
|
+
}
|
|
111
|
+
RED.popover.tooltip(toolbarMenuButtonAnchor, plugin._('name'))
|
|
112
|
+
|
|
113
|
+
/* NOTE: For the menu entries icons' property...
|
|
114
|
+
If `.icon` is a URL (e.g. resource/xxx/icon.svg), the RED.menu API will add it as an <img> tag.
|
|
115
|
+
That makes it impossible to set the fill colour of the SVG PATH via a CSS var.
|
|
116
|
+
So, by not specifying an icon URL, an <i> tag with the class set to <icon> will be created by the API
|
|
117
|
+
This permits us to use CSS classes (defined below) that can set the icon and affect the fill colour
|
|
118
|
+
*/
|
|
119
|
+
debug('Building FlowFuse Expert menu')
|
|
120
|
+
|
|
121
|
+
const ffAssistantMenu = [
|
|
122
|
+
{ id: 'ff-assistant-title', label: plugin._('name'), visible: true }, // header
|
|
123
|
+
null // separator
|
|
124
|
+
]
|
|
125
|
+
RED.menu.init({ id: 'red-ui-header-button-ff-ai', options: ffAssistantMenu })
|
|
126
|
+
}
|
|
127
|
+
function showLoginPrompt () {
|
|
128
|
+
$.ajax({
|
|
129
|
+
contentType: 'application/json',
|
|
130
|
+
url: 'nr-assistant/auth/start',
|
|
131
|
+
method: 'POST',
|
|
132
|
+
data: JSON.stringify({
|
|
133
|
+
editorURL: window.location.origin + window.location.pathname
|
|
134
|
+
})
|
|
135
|
+
}).then(data => {
|
|
136
|
+
if (data && data.path && data.state) {
|
|
137
|
+
const handleAuthCallback = function (evt) {
|
|
138
|
+
debug('handleAuthCallback', evt)
|
|
139
|
+
try {
|
|
140
|
+
const message = JSON.parse(evt.data)
|
|
141
|
+
if (message.code === 'flowfuse-auth-complete') {
|
|
142
|
+
showNotification('Connected to FlowFuse', { type: 'success' })
|
|
143
|
+
if (message.state === data.state) {
|
|
144
|
+
RED.menu.setVisible('ff-assistant-login', false)
|
|
145
|
+
RED.menu.setVisible('ff-assistant-function-builder', true)
|
|
146
|
+
}
|
|
147
|
+
} else if (message.code === 'flowfuse-auth-error') {
|
|
148
|
+
showNotification('Failed to connect to FlowFuse', { type: 'error' })
|
|
149
|
+
console.warn('Failed to connect to FlowFuse:', message.error)
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {}
|
|
152
|
+
window.removeEventListener('message', handleAuthCallback, false)
|
|
153
|
+
}
|
|
154
|
+
window.open(document.location.toString().replace(/[?#].*$/, '') + data.path, 'FlowFuseNodeREDPluginAuthWindow', 'menubar=no,location=no,toolbar=no,chrome,height=650,width=500')
|
|
155
|
+
window.addEventListener('message', handleAuthCallback, false)
|
|
156
|
+
} else if (data && data.error) {
|
|
157
|
+
RED.notify(`Failed to connect to server: ${data.error}`, { type: 'error' })
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
99
161
|
function initAssistant (options) {
|
|
100
|
-
|
|
101
|
-
|
|
162
|
+
debug('initialising...', assistantOptions)
|
|
163
|
+
if (!initialisedInterlock) {
|
|
164
|
+
// Initialiise common UI elements
|
|
165
|
+
initialisedInterlock = true
|
|
166
|
+
createAssistantMenu()
|
|
167
|
+
if (assistantOptions.standalone) {
|
|
168
|
+
RED.menu.addItem('red-ui-header-button-ff-ai', {
|
|
169
|
+
id: 'ff-assistant-login',
|
|
170
|
+
label: `<span>${plugin._('login.menu.label')}</span>`,
|
|
171
|
+
onselect: showLoginPrompt,
|
|
172
|
+
visible: !assistantOptions.enabled
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
RED.menu.addItem('red-ui-header-button-ff-ai', {
|
|
176
|
+
id: 'ff-assistant-function-builder',
|
|
177
|
+
icon: 'ff-assistant-menu-icon function',
|
|
178
|
+
label: `<span>${plugin._('function-builder.menu.label')}</span>`,
|
|
179
|
+
sublabel: plugin._('function-builder.menu.description'),
|
|
180
|
+
onselect: 'flowfuse-nr-assistant:function-builder',
|
|
181
|
+
shortcutSpan: $('<span class="red-ui-popover-key"></span>'),
|
|
182
|
+
visible: assistantOptions.enabled
|
|
183
|
+
})
|
|
102
184
|
}
|
|
103
|
-
|
|
185
|
+
|
|
104
186
|
if (!assistantOptions.enabled) {
|
|
187
|
+
if (assistantOptions.standalone) {
|
|
188
|
+
RED.menu.setVisible('ff-assistant-login', true)
|
|
189
|
+
}
|
|
190
|
+
RED.menu.setVisible('ff-assistant-function-builder', false)
|
|
105
191
|
console.warn(plugin._('errors.assistant-not-enabled'))
|
|
106
192
|
return
|
|
193
|
+
} else {
|
|
194
|
+
if (assistantOptions.standalone) {
|
|
195
|
+
RED.menu.setVisible('ff-assistant-login', false)
|
|
196
|
+
}
|
|
197
|
+
RED.menu.setVisible('ff-assistant-function-builder', true)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!assistantInitialised) {
|
|
201
|
+
registerMonacoExtensions()
|
|
202
|
+
RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:function-builder.action.label' })
|
|
203
|
+
setMenuShortcutKey('ff-assistant-function-builder', 'red-ui-workspace', 'ctrl-alt-f', 'flowfuse-nr-assistant:function-builder')
|
|
204
|
+
assistantInitialised = true
|
|
107
205
|
}
|
|
206
|
+
}
|
|
108
207
|
|
|
208
|
+
function registerMonacoExtensions () {
|
|
109
209
|
if (!window.monaco) {
|
|
110
210
|
console.warn('Monaco editor not found. Unable to register code lens provider. Consider using the Monaco editor for a better experience.')
|
|
111
211
|
return
|
|
112
212
|
}
|
|
113
|
-
|
|
114
213
|
const funcCommandId = 'nr-assistant-fn-inline'
|
|
115
214
|
const jsonCommandId = 'nr-assistant-json-inline'
|
|
116
215
|
const cssCommandId = 'nr-assistant-css-inline'
|
|
@@ -121,6 +220,9 @@
|
|
|
121
220
|
|
|
122
221
|
monaco.languages.registerCodeLensProvider('javascript', {
|
|
123
222
|
provideCodeLenses: function (model, token) {
|
|
223
|
+
if (!assistantOptions.enabled) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
124
226
|
const thisEditor = getMonacoEditorForModel(model)
|
|
125
227
|
if (!thisEditor) {
|
|
126
228
|
return
|
|
@@ -168,8 +270,8 @@
|
|
|
168
270
|
}
|
|
169
271
|
codeLens.command = {
|
|
170
272
|
id: codeLens.id,
|
|
171
|
-
title: 'Ask the FlowFuse
|
|
172
|
-
tooltip: 'Click to ask FlowFuse
|
|
273
|
+
title: 'Ask the FlowFuse Expert 🪄',
|
|
274
|
+
tooltip: 'Click to ask FlowFuse Expert for help writing code',
|
|
173
275
|
arguments: [model, codeLens, token]
|
|
174
276
|
}
|
|
175
277
|
return codeLens
|
|
@@ -178,6 +280,9 @@
|
|
|
178
280
|
|
|
179
281
|
monaco.languages.registerCodeLensProvider('json', {
|
|
180
282
|
provideCodeLenses: function (model, token) {
|
|
283
|
+
if (!assistantOptions.enabled) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
181
286
|
const thisEditor = getMonacoEditorForModel(model)
|
|
182
287
|
if (!thisEditor) {
|
|
183
288
|
return
|
|
@@ -203,8 +308,8 @@
|
|
|
203
308
|
}
|
|
204
309
|
codeLens.command = {
|
|
205
310
|
id: codeLens.id,
|
|
206
|
-
title: 'Ask the FlowFuse
|
|
207
|
-
tooltip: 'Click to ask FlowFuse
|
|
311
|
+
title: 'Ask the FlowFuse Expert 🪄',
|
|
312
|
+
tooltip: 'Click to ask FlowFuse Expert for help with JSON',
|
|
208
313
|
arguments: [model, codeLens, token]
|
|
209
314
|
}
|
|
210
315
|
return codeLens
|
|
@@ -213,6 +318,9 @@
|
|
|
213
318
|
|
|
214
319
|
monaco.languages.registerCodeLensProvider('css', {
|
|
215
320
|
provideCodeLenses: function (model, token) {
|
|
321
|
+
if (!assistantOptions.enabled) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
216
324
|
debug('CSS CodeLens provider called', model, token)
|
|
217
325
|
const thisEditor = getMonacoEditorForModel(model)
|
|
218
326
|
const node = RED.view.selection()?.nodes?.[0]
|
|
@@ -242,8 +350,8 @@
|
|
|
242
350
|
}
|
|
243
351
|
codeLens.command = {
|
|
244
352
|
id: codeLens.id,
|
|
245
|
-
title: 'Ask the FlowFuse
|
|
246
|
-
tooltip: 'Click to ask FlowFuse
|
|
353
|
+
title: 'Ask the FlowFuse Expert 🪄',
|
|
354
|
+
tooltip: 'Click to ask FlowFuse Expert for help with CSS',
|
|
247
355
|
arguments: [model, codeLens, token]
|
|
248
356
|
}
|
|
249
357
|
return codeLens
|
|
@@ -252,6 +360,9 @@
|
|
|
252
360
|
|
|
253
361
|
monaco.languages.registerCodeLensProvider('html', {
|
|
254
362
|
provideCodeLenses: function (model, token) {
|
|
363
|
+
if (!assistantOptions.enabled) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
255
366
|
debug('HTML CodeLens provider called', model, token)
|
|
256
367
|
const thisEditor = getMonacoEditorForModel(model)
|
|
257
368
|
if (!thisEditor) {
|
|
@@ -284,8 +395,8 @@
|
|
|
284
395
|
}
|
|
285
396
|
codeLens.command = {
|
|
286
397
|
id: codeLens.id,
|
|
287
|
-
title: 'Ask the FlowFuse
|
|
288
|
-
tooltip: 'Click to ask FlowFuse
|
|
398
|
+
title: 'Ask the FlowFuse Expert 🪄',
|
|
399
|
+
tooltip: 'Click to ask FlowFuse Expert for help with VUE or HTML',
|
|
289
400
|
arguments: [model, codeLens, token]
|
|
290
401
|
}
|
|
291
402
|
return codeLens
|
|
@@ -294,6 +405,9 @@
|
|
|
294
405
|
|
|
295
406
|
assistantOptions.tablesEnabled && monaco.languages.registerCodeLensProvider('sql', {
|
|
296
407
|
provideCodeLenses: function (model, token) {
|
|
408
|
+
if (!assistantOptions.enabled) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
297
411
|
debug('SQL CodeLens provider called', model, token)
|
|
298
412
|
const thisEditor = getMonacoEditorForModel(model)
|
|
299
413
|
if (!thisEditor) {
|
|
@@ -326,8 +440,8 @@
|
|
|
326
440
|
}
|
|
327
441
|
codeLens.command = {
|
|
328
442
|
id: codeLens.id,
|
|
329
|
-
title: 'Ask the FlowFuse
|
|
330
|
-
tooltip: 'Click to ask FlowFuse
|
|
443
|
+
title: 'Ask the FlowFuse Expert 🪄',
|
|
444
|
+
tooltip: 'Click to ask FlowFuse Expert for help with PostgreSQL',
|
|
331
445
|
arguments: [model, codeLens, token]
|
|
332
446
|
}
|
|
333
447
|
return codeLens
|
|
@@ -365,8 +479,8 @@
|
|
|
365
479
|
}
|
|
366
480
|
/** @type {PromptUIOptions} */
|
|
367
481
|
const uiOptions = {
|
|
368
|
-
title: 'FlowFuse
|
|
369
|
-
explanation: 'The FlowFuse
|
|
482
|
+
title: 'FlowFuse Expert : Function Code',
|
|
483
|
+
explanation: 'The FlowFuse Expert can help you write JavaScript code.',
|
|
370
484
|
description: 'Enter a short description of what you want it to do.'
|
|
371
485
|
}
|
|
372
486
|
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
|
|
@@ -451,8 +565,8 @@
|
|
|
451
565
|
}
|
|
452
566
|
/** @type {PromptUIOptions} */
|
|
453
567
|
const uiOptions = {
|
|
454
|
-
title: 'FlowFuse
|
|
455
|
-
explanation: 'The FlowFuse
|
|
568
|
+
title: 'FlowFuse Expert : JSON',
|
|
569
|
+
explanation: 'The FlowFuse Expert can help you write JSON.',
|
|
456
570
|
description: 'Enter a short description of what you want it to do.'
|
|
457
571
|
}
|
|
458
572
|
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
|
|
@@ -513,8 +627,8 @@
|
|
|
513
627
|
}
|
|
514
628
|
/** @type {PromptUIOptions} */
|
|
515
629
|
const uiOptions = {
|
|
516
|
-
title: 'FlowFuse
|
|
517
|
-
explanation: 'The FlowFuse
|
|
630
|
+
title: 'FlowFuse Expert : CSS',
|
|
631
|
+
explanation: 'The FlowFuse Expert can help you write CSS.',
|
|
518
632
|
description: 'Enter a short description of what you want it to do.'
|
|
519
633
|
}
|
|
520
634
|
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
|
|
@@ -575,8 +689,8 @@
|
|
|
575
689
|
}
|
|
576
690
|
/** @type {PromptUIOptions} */
|
|
577
691
|
const uiOptions = {
|
|
578
|
-
title: 'FlowFuse
|
|
579
|
-
explanation: 'The FlowFuse
|
|
692
|
+
title: 'FlowFuse Expert : Dashboard 2 UI Template',
|
|
693
|
+
explanation: 'The FlowFuse Expert can help you write HTML, VUE and JavaScript.',
|
|
580
694
|
description: 'Enter a short description of what you want it to do.'
|
|
581
695
|
}
|
|
582
696
|
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
|
|
@@ -638,8 +752,8 @@
|
|
|
638
752
|
}
|
|
639
753
|
/** @type {PromptUIOptions} */
|
|
640
754
|
const uiOptions = {
|
|
641
|
-
title: 'FlowFuse
|
|
642
|
-
explanation: 'The FlowFuse
|
|
755
|
+
title: 'FlowFuse Expert : FlowFuse Query',
|
|
756
|
+
explanation: 'The FlowFuse Expert can help you write SQL queries.',
|
|
643
757
|
description: 'Enter a short description of what you want it to do.'
|
|
644
758
|
}
|
|
645
759
|
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
|
|
@@ -946,7 +1060,9 @@
|
|
|
946
1060
|
provideInlineCompletions: (model, position, context, token) =>
|
|
947
1061
|
new Promise((resolve) => {
|
|
948
1062
|
debug('provideInlineCompletions: handling inline completion for languageId ' + languageId, position, context, token)
|
|
949
|
-
|
|
1063
|
+
if (!assistantOptions.enabled) {
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
950
1066
|
// prep - get the editor and node, exit if not found or not supported
|
|
951
1067
|
const { editor, node } = getEditorAndNode(model)
|
|
952
1068
|
if (!editor || !node) { return }
|
|
@@ -1025,41 +1141,7 @@
|
|
|
1025
1141
|
}
|
|
1026
1142
|
supportedInlineCompletions.forEach(options => registerAIInlineCompletions(options))
|
|
1027
1143
|
}
|
|
1028
|
-
|
|
1029
|
-
const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button" href="#"></a></li>')
|
|
1030
|
-
const toolbarMenuButtonAnchor = toolbarMenuButton.find('a')
|
|
1031
|
-
const deployButtonLi = $('#red-ui-header-button-deploy').closest('li')
|
|
1032
|
-
if (deployButtonLi.length) {
|
|
1033
|
-
deployButtonLi.before(toolbarMenuButton) // add the button before the deploy button
|
|
1034
|
-
} else {
|
|
1035
|
-
toolbarMenuButton.prependTo('.red-ui-header-toolbar') // add the button leftmost of the toolbar
|
|
1036
|
-
}
|
|
1037
|
-
RED.popover.tooltip(toolbarMenuButtonAnchor, plugin._('name'))
|
|
1038
|
-
|
|
1039
|
-
/* NOTE: For the menu entries icons' property...
|
|
1040
|
-
If `.icon` is a URL (e.g. resource/xxx/icon.svg), the RED.menu API will add it as an <img> tag.
|
|
1041
|
-
That makes it impossible to set the fill colour of the SVG PATH via a CSS var.
|
|
1042
|
-
So, by not specifying an icon URL, an <i> tag with the class set to <icon> will be created by the API
|
|
1043
|
-
This permits us to use CSS classes (defined below) that can set the icon and affect the fill colour
|
|
1044
|
-
*/
|
|
1045
|
-
debug('Building FlowFuse Assistant menu')
|
|
1046
|
-
const ffAssistantMenu = [
|
|
1047
|
-
{ id: 'ff-assistant-title', label: plugin._('name'), visible: true }, // header
|
|
1048
|
-
null, // separator
|
|
1049
|
-
{
|
|
1050
|
-
id: 'ff-assistant-function-builder',
|
|
1051
|
-
icon: 'ff-assistant-menu-icon function',
|
|
1052
|
-
label: `<span>${plugin._('function-builder.menu.label')}</span>`,
|
|
1053
|
-
sublabel: plugin._('function-builder.menu.description'),
|
|
1054
|
-
onselect: 'flowfuse-nr-assistant:function-builder',
|
|
1055
|
-
shortcutSpan: $('<span class="red-ui-popover-key"></span>'),
|
|
1056
|
-
visible: true
|
|
1057
|
-
}
|
|
1058
|
-
]
|
|
1059
|
-
RED.menu.init({ id: 'red-ui-header-button-ff-ai', options: ffAssistantMenu })
|
|
1060
|
-
assistantInitialised = true
|
|
1061
1144
|
}
|
|
1062
|
-
|
|
1063
1145
|
const previousPrompts = {}
|
|
1064
1146
|
|
|
1065
1147
|
/**
|
|
@@ -1151,7 +1233,7 @@
|
|
|
1151
1233
|
|
|
1152
1234
|
function getUserInput ({ title, explanation, description, placeholder, defaultInput } = {
|
|
1153
1235
|
title: plugin._('name'),
|
|
1154
|
-
explanation: 'The FlowFuse
|
|
1236
|
+
explanation: 'The FlowFuse Expert can help you create things.',
|
|
1155
1237
|
description: 'Enter a short description explaining what you want it to do.',
|
|
1156
1238
|
placeholder: '',
|
|
1157
1239
|
defaultInput: ''
|
|
@@ -1200,7 +1282,7 @@
|
|
|
1200
1282
|
},
|
|
1201
1283
|
buttons: [
|
|
1202
1284
|
{
|
|
1203
|
-
text: 'Ask the FlowFuse
|
|
1285
|
+
text: 'Ask the FlowFuse Expert 🪄',
|
|
1204
1286
|
// class: 'primary',
|
|
1205
1287
|
click: function () {
|
|
1206
1288
|
const prompt = dialog.find('#ff-nr-ai-dialog-input-editor').val()
|
|
@@ -1225,7 +1307,7 @@
|
|
|
1225
1307
|
}
|
|
1226
1308
|
getUserInput({
|
|
1227
1309
|
defaultInput: previousFunctionBuilderPrompt,
|
|
1228
|
-
title: title || 'FlowFuse
|
|
1310
|
+
title: title || 'FlowFuse Expert : Create A Function Node',
|
|
1229
1311
|
explanation: plugin._('function-builder.dialog-input.explanation'),
|
|
1230
1312
|
description: plugin._('function-builder.dialog-input.description')
|
|
1231
1313
|
}).then((prompt) => {
|
|
@@ -1421,7 +1503,7 @@
|
|
|
1421
1503
|
const options = {
|
|
1422
1504
|
buttons: [{
|
|
1423
1505
|
text: plugin._('explain-flows.dialog-result.close-button'),
|
|
1424
|
-
icon: 'ui-icon-close',
|
|
1506
|
+
// icon: 'ui-icon-close',
|
|
1425
1507
|
class: 'primary',
|
|
1426
1508
|
click: function () {
|
|
1427
1509
|
$(dlg).dialog('close')
|
|
@@ -1435,7 +1517,7 @@
|
|
|
1435
1517
|
options.buttons.unshift(
|
|
1436
1518
|
{
|
|
1437
1519
|
text: plugin._('explain-flows.dialog-result.copy-button'),
|
|
1438
|
-
icon: 'ui-icon-copy',
|
|
1520
|
+
// icon: 'ui-icon-copy',
|
|
1439
1521
|
click: function () {
|
|
1440
1522
|
navigator.clipboard.writeText(text).then(() => {
|
|
1441
1523
|
showNotification('Copied to clipboard', { type: 'success' })
|
|
@@ -1452,7 +1534,7 @@
|
|
|
1452
1534
|
options.buttons.unshift(
|
|
1453
1535
|
{
|
|
1454
1536
|
text: plugin._('explain-flows.dialog-result.comment-node-button'),
|
|
1455
|
-
icon: 'ui-icon-comment',
|
|
1537
|
+
// icon: 'ui-icon-comment',
|
|
1456
1538
|
// class: 'primary',
|
|
1457
1539
|
click: function () {
|
|
1458
1540
|
const commentNode = {
|
|
@@ -1502,8 +1584,8 @@
|
|
|
1502
1584
|
}
|
|
1503
1585
|
getUserInput({
|
|
1504
1586
|
defaultInput: previousFlowBuilderPrompt,
|
|
1505
|
-
title: title || 'FlowFuse
|
|
1506
|
-
explanation: 'The FlowFuse
|
|
1587
|
+
title: title || 'FlowFuse Expert : Flow Builder',
|
|
1588
|
+
explanation: 'The FlowFuse Expert can help you create a new flow.',
|
|
1507
1589
|
description: 'Enter a short description of what you want the flow to do.'
|
|
1508
1590
|
}).then((prompt) => {
|
|
1509
1591
|
/** @type {JQueryXHR} */
|
|
@@ -1589,7 +1671,7 @@
|
|
|
1589
1671
|
}
|
|
1590
1672
|
|
|
1591
1673
|
/**
|
|
1592
|
-
* Shows a
|
|
1674
|
+
* Shows a notification
|
|
1593
1675
|
* @param {string} message - The message to display in the notification
|
|
1594
1676
|
* @param {object} [options] - The options to pass to the notification
|
|
1595
1677
|
* @param {'error'|'warning'|'success'|'compact'} [options.type] - The type of notification to display
|
|
@@ -1663,25 +1745,25 @@
|
|
|
1663
1745
|
const minutes = Math.floor(seconds / 60)
|
|
1664
1746
|
const remainingSeconds = seconds % 60
|
|
1665
1747
|
if (minutes > 0) {
|
|
1666
|
-
RED.notify(`Sorry, the FlowFuse
|
|
1748
|
+
RED.notify(`Sorry, the FlowFuse Expert is busy. Please try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`, 'warning')
|
|
1667
1749
|
} else {
|
|
1668
|
-
RED.notify(`Sorry, the FlowFuse
|
|
1750
|
+
RED.notify(`Sorry, the FlowFuse Expert is busy. Please try again in ${remainingSeconds} second${remainingSeconds > 1 ? 's' : ''}.`, 'warning')
|
|
1669
1751
|
}
|
|
1670
1752
|
return
|
|
1671
1753
|
}
|
|
1672
|
-
RED.notify('Sorry, the FlowFuse
|
|
1754
|
+
RED.notify('Sorry, the FlowFuse Expert is busy. Please try again later.', 'warning')
|
|
1673
1755
|
return
|
|
1674
1756
|
}
|
|
1675
1757
|
if (jqXHR.status === 404) {
|
|
1676
|
-
RED.notify('Sorry, the FlowFuse
|
|
1758
|
+
RED.notify('Sorry, the FlowFuse Expert is not available at the moment', 'warning')
|
|
1677
1759
|
return
|
|
1678
1760
|
}
|
|
1679
1761
|
if (jqXHR.status === 401) {
|
|
1680
|
-
RED.notify('Sorry, you are not authorised to use the FlowFuse
|
|
1762
|
+
RED.notify('Sorry, you are not authorised to use the FlowFuse Expert', 'warning')
|
|
1681
1763
|
return
|
|
1682
1764
|
}
|
|
1683
1765
|
if (jqXHR.status >= 400 && jqXHR.status < 500) {
|
|
1684
|
-
let message = 'Sorry, the FlowFuse
|
|
1766
|
+
let message = 'Sorry, the FlowFuse Expert cannot help with this request'
|
|
1685
1767
|
if (jqXHR.responseJSON?.body?.code === 'assistant_service_denied' && jqXHR.responseJSON?.body?.error) {
|
|
1686
1768
|
message = jqXHR.responseJSON.body.error
|
|
1687
1769
|
}
|
|
@@ -1907,7 +1989,7 @@
|
|
|
1907
1989
|
#red-ui-header ul#red-ui-header-button-ff-ai-submenu.red-ui-menu-dropdown {
|
|
1908
1990
|
width: 300px !important; /* make the submenu wider to prevent wrapping elements */
|
|
1909
1991
|
}
|
|
1910
|
-
#red-ui-header ul.red-ui-menu-dropdown li a {
|
|
1992
|
+
#red-ui-header #red-ui-header-button-ff-ai+ul.red-ui-menu-dropdown li a {
|
|
1911
1993
|
padding: 8px 16px;
|
|
1912
1994
|
}
|
|
1913
1995
|
|
package/index.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
module.exports = (RED) => {
|
|
2
2
|
const assistant = require('./lib/assistant.js')
|
|
3
|
-
|
|
3
|
+
const settings = require('./lib/settings.js')
|
|
4
|
+
const auth = require('./lib/auth/index.js')
|
|
4
5
|
RED.plugins.registerPlugin('flowfuse-nr-assistant', {
|
|
5
6
|
type: 'assistant',
|
|
6
|
-
name: 'Node-RED
|
|
7
|
+
name: 'Node-RED Expert Plugin',
|
|
7
8
|
icon: 'font-awesome/fa-magic',
|
|
8
9
|
settings: {
|
|
9
10
|
'*': { exportable: true }
|
|
10
11
|
},
|
|
11
|
-
onadd: function () {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
onadd: async function () {
|
|
13
|
+
try {
|
|
14
|
+
await auth.init(RED)
|
|
15
|
+
const assistantSettings = await settings.getSettings(RED)
|
|
16
|
+
if (!assistant.isInitialized && !assistant.isLoading) {
|
|
17
|
+
assistant.init(RED, assistantSettings).then(() => {
|
|
18
|
+
// All good, the assistant is initialized.
|
|
19
|
+
// Any info messages made during initialization are logged in the assistant module
|
|
20
|
+
}).catch((error) => {
|
|
21
|
+
console.error(error)
|
|
22
|
+
RED.log.error('Failed to initialize FlowFuse Expert Plugin:', error)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(error)
|
|
27
|
+
RED.log.error('Failed to initialize FlowFuse Expert Plugin:', error)
|
|
20
28
|
}
|
|
21
29
|
}
|
|
22
30
|
})
|