@flowfuse/nr-assistant 0.2.2-28e5c9f-202506121535.0 → 0.2.2-480c08b-202506180948.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 CHANGED
@@ -26,7 +26,7 @@
26
26
  let assistantInitialised = false
27
27
  let assistantMCPOneTimeFlag = false
28
28
  debug('loading...')
29
- RED.plugins.registerPlugin('flowfuse-nr-assistant', {
29
+ const plugin = {
30
30
  type: 'assistant',
31
31
  name: 'Node-RED Assistant Plugin',
32
32
  icon: 'font-awesome/fa-magic',
@@ -37,17 +37,31 @@
37
37
  assistantOptions.enabled = !!msg?.enabled
38
38
  assistantOptions.requestTimeout = msg?.requestTimeout || AI_TIMEOUT
39
39
  initAssistant(msg)
40
+ RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:function-builder.action.label' })
41
+ setMenuShortcutKey('ff-assistant-function-builder', 'red-ui-workspace', 'ctrl-alt-f', 'flowfuse-nr-assistant:function-builder')
40
42
  }
41
43
  if (topic === 'nr-assistant/mcp/ready' && !assistantMCPOneTimeFlag) {
42
44
  assistantMCPOneTimeFlag = true
43
45
  if (assistantOptions.enabled) {
44
46
  debug('assistant MCP initialised')
45
- RED.actions.add('flowfuse-nr-assistant:explain-selected-nodes', explainSelectedNodes, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:actions.explain-selected-nodes' })
47
+ RED.actions.add('flowfuse-nr-assistant:explain-flows', explainSelectedNodes, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:explain-flows.action.label' })
48
+ const menuEntry = {
49
+ id: 'ff-assistant-explain-flows',
50
+ icon: 'ff-assistant-menu-icon explain-flows',
51
+ label: `<span>${plugin._('explain-flows.menu.label')}</span>`,
52
+ sublabel: plugin._('explain-flows.menu.description'),
53
+ onselect: 'flowfuse-nr-assistant:explain-flows',
54
+ shortcutSpan: $('<span class="red-ui-popover-key"></span>'),
55
+ visible: true
56
+ }
57
+ RED.menu.addItem('red-ui-header-button-ff-ai', menuEntry)
58
+ setMenuShortcutKey('ff-assistant-explain-flows', 'red-ui-workspace', 'ctrl-alt-e', 'flowfuse-nr-assistant:explain-flows')
46
59
  }
47
60
  }
48
61
  })
49
62
  }
50
- })
63
+ }
64
+ RED.plugins.registerPlugin('flowfuse-nr-assistant', plugin)
51
65
 
52
66
  function initAssistant () {
53
67
  if (assistantInitialised) {
@@ -55,7 +69,7 @@
55
69
  }
56
70
  debug('initialising...')
57
71
  if (!assistantOptions.enabled) {
58
- console.warn('The FlowFuse Assistant is not enabled')
72
+ console.warn(plugin._('errors.assistant-not-enabled'))
59
73
  return
60
74
  }
61
75
 
@@ -171,7 +185,7 @@
171
185
  return
172
186
  }
173
187
  if (!assistantOptions.enabled) {
174
- RED.notify('The FlowFuse Assistant is not enabled', 'warning')
188
+ RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
175
189
  return
176
190
  }
177
191
  const thisEditor = getMonacoEditorForModel(model)
@@ -300,7 +314,7 @@
300
314
  return
301
315
  }
302
316
  if (!assistantOptions.enabled) {
303
- RED.notify('The FlowFuse Assistant is not enabled', 'warning')
317
+ RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
304
318
  return
305
319
  }
306
320
  const thisEditor = getMonacoEditorForModel(model)
@@ -354,30 +368,37 @@
354
368
  }
355
369
  })
356
370
 
357
- // setup actions for function builder
358
- RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:actions.function-builder' })
359
-
360
- // FUTURE: setup actions for flow builder
361
- // const flowBuilderTitle = 'FlowFuse Flow Assistant'
362
- // RED.actions.add('ff:assistant-flow-builder', showFlowBuilderPrompt, { label: flowBuilderTitle })
363
-
364
371
  const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button" href="#"></a></li>')
365
372
  const toolbarMenuButtonAnchor = toolbarMenuButton.find('a')
366
- toolbarMenuButtonAnchor.css('mask-image', 'url("resources/@flowfuse/nr-assistant/assistant-button.svg")')
367
- toolbarMenuButtonAnchor.css('mask-repeat', 'no-repeat')
368
- toolbarMenuButtonAnchor.css('mask-position', 'center')
369
- toolbarMenuButtonAnchor.css('background-color', 'currentColor')
370
- toolbarMenuButtonAnchor.css('height', '28px') // for backwards compatibility with <= NR3.x inline-block toolbar styling
371
373
  const deployButtonLi = $('#red-ui-header-button-deploy').closest('li')
372
374
  if (deployButtonLi.length) {
373
375
  deployButtonLi.before(toolbarMenuButton) // add the button before the deploy button
374
376
  } else {
375
377
  toolbarMenuButton.prependTo('.red-ui-header-toolbar') // add the button leftmost of the toolbar
376
378
  }
377
- toolbarMenuButtonAnchor.on('click', function (e) {
378
- RED.actions.invoke('flowfuse-nr-assistant:function-builder')
379
- })
380
- RED.popover.tooltip(toolbarMenuButtonAnchor, 'FlowFuse Assistant')
379
+ RED.popover.tooltip(toolbarMenuButtonAnchor, plugin._('name'))
380
+
381
+ /* NOTE: For the menu entries icons' property...
382
+ If `.icon` is a URL (e.g. resource/xxx/icon.svg), the RED.menu API will add it as an <img> tag.
383
+ That makes it impossible to set the fill colour of the SVG PATH via a CSS var.
384
+ So, by not specifying an icon URL, an <i> tag with the class set to <icon> will be created by the API
385
+ This permits us to use CSS classes (defined below) that can set the icon and affect the fill colour
386
+ */
387
+ debug('Building FlowFuse Assistant menu')
388
+ const ffAssistantMenu = [
389
+ { id: 'ff-assistant-title', label: plugin._('name'), visible: true }, // header
390
+ null, // separator
391
+ {
392
+ id: 'ff-assistant-function-builder',
393
+ icon: 'ff-assistant-menu-icon function',
394
+ label: `<span>${plugin._('function-builder.menu.label')}</span>`,
395
+ sublabel: plugin._('function-builder.menu.description'),
396
+ onselect: 'flowfuse-nr-assistant:function-builder',
397
+ shortcutSpan: $('<span class="red-ui-popover-key"></span>'),
398
+ visible: true
399
+ }
400
+ ]
401
+ RED.menu.init({ id: 'red-ui-header-button-ff-ai', options: ffAssistantMenu })
381
402
  assistantInitialised = true
382
403
  }
383
404
 
@@ -409,8 +430,8 @@
409
430
  debug('doPrompt', promptOptions, uiOptions)
410
431
  getUserInput({
411
432
  defaultInput: defaultInput || '',
412
- title: uiOptions?.title || 'FlowFuse Assistant',
413
- explanation: uiOptions?.explanation || 'The FlowFuse Assistant can help you write code.',
433
+ title: uiOptions?.title || plugin._('name'),
434
+ explanation: uiOptions?.explanation || plugin._('name') + ' can help you write code.',
414
435
  description: uiOptions?.description || 'Enter a short description of what you want it to do.'
415
436
  }).then((prompt) => {
416
437
  if (!prompt) {
@@ -429,7 +450,7 @@
429
450
  // selection: selectedText // FUTURE: include the selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
430
451
  }
431
452
  }
432
- const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
453
+ const busyNotification = showBusyNotification(plugin._('notifications.busy'), function () {
433
454
  if (xhr) {
434
455
  xhr.abort('abort')
435
456
  xhr = null
@@ -471,7 +492,7 @@
471
492
  }
472
493
 
473
494
  function getUserInput ({ title, explanation, description, placeholder, defaultInput } = {
474
- title: 'FlowFuse Assistant',
495
+ title: plugin._('name'),
475
496
  explanation: 'The FlowFuse Assistant can help you create things.',
476
497
  description: 'Enter a short description explaining what you want it to do.',
477
498
  placeholder: '',
@@ -498,9 +519,8 @@
498
519
  dialog.append(containerDiv)
499
520
  const minHeight = 260 + (description ? 32 : 0) + (explanation ? 32 : 0)
500
521
  const minWidth = 480
501
- dialog.dialog({
502
- autoOpen: true,
503
- title: title || 'FlowFuse Assistant',
522
+ const dialogControl = dialog.dialog({
523
+ title: title || plugin._('name'),
504
524
  modal: true,
505
525
  closeOnEscape: true,
506
526
  height: minHeight,
@@ -509,6 +529,8 @@
509
529
  minWidth,
510
530
  resizable: true,
511
531
  draggable: true,
532
+ show: { effect: 'fade', duration: 300 },
533
+ hide: { effect: 'fade', duration: 300 },
512
534
  open: function (event, ui) {
513
535
  RED.keyboard.disable()
514
536
  input.focus()
@@ -516,6 +538,7 @@
516
538
  },
517
539
  close: function (event, ui) {
518
540
  RED.keyboard.enable()
541
+ dialogControl.remove()
519
542
  },
520
543
  buttons: [
521
544
  {
@@ -539,14 +562,14 @@
539
562
  let previousFunctionBuilderPrompt
540
563
  function showFunctionBuilderPrompt (title) {
541
564
  if (!assistantOptions.enabled) {
542
- RED.notify('The FlowFuse Assistant is not enabled', 'warning')
565
+ RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
543
566
  return
544
567
  }
545
568
  getUserInput({
546
569
  defaultInput: previousFunctionBuilderPrompt,
547
570
  title: title || 'FlowFuse Assistant : Create A Function Node',
548
- explanation: 'The FlowFuse Assistant can help you create a Function Node.',
549
- description: 'Enter a short description of what you want it to do.'
571
+ explanation: plugin._('function-builder.dialog-input.explanation'),
572
+ description: plugin._('function-builder.dialog-input.description')
550
573
  }).then((prompt) => {
551
574
  /** @type {JQueryXHR} */
552
575
  let xhr = null
@@ -562,7 +585,7 @@
562
585
  modulesAllowed
563
586
  }
564
587
  }
565
- const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
588
+ const busyNotification = showBusyNotification(plugin._('notifications.busy'), function () {
566
589
  if (xhr) {
567
590
  xhr.abort('abort')
568
591
  xhr = null
@@ -601,18 +624,104 @@
601
624
  })
602
625
  }
603
626
 
627
+ /**
628
+ * Show a dialog with the given title and content.
629
+ * * NOTE: Only basic sanitization is performed. Call RED.utils.renderMarkdown or RED.utils.sanitize before passing it in.
630
+ * @param {string} title - The title of the dialog
631
+ * @param {string} content - The content of the dialog, can be HTML
632
+ * @param {'text'|'html'} [renderMode='text'] - Whether the content is HTML or plain text.
633
+ * @param {object} [options] - jQuery UI Options for the dialog
634
+ */
635
+ function showMessage (title, content, renderMode = 'text', options = {}) {
636
+ const window70vw = $(window).width() * 0.70 // 70% of viewport width
637
+ const window70vh = $(window).height() * 0.70 // 70% of viewport height
638
+
639
+ // super basic sanitisation for xss in content
640
+ if (typeof content !== 'string' || content.length === 0) {
641
+ console.warn('Content must be a string')
642
+ return
643
+ } else if (renderMode === 'html' && (content.includes('<script>') || content.includes('<' + '/script>'))) {
644
+ content = content.replace(/<script>/g, '```\n').replace(/<\/script>/g, '```') // basic sanitization
645
+ }
646
+
647
+ // First some basic sizing calculations to ensure the dialog is not too small/large
648
+ let initialMaxWidth = $('#red-ui-notifications').width() || 500 // start off with 500px as the initial maxWidth
649
+ if (content.length > 3000) {
650
+ initialMaxWidth = 1000
651
+ } else if (content.length > 2000) {
652
+ initialMaxWidth = 850
653
+ } else if (content.length > 1500) {
654
+ initialMaxWidth = 700
655
+ } else if (content.length > 750) {
656
+ initialMaxWidth = 600
657
+ }
658
+
659
+ // Now render the content as HTML so later we can gauge its size and dynamically adjust to keep it reasonable
660
+ const $dialog = $('<div>')
661
+ $dialog.css({
662
+ overflow: 'auto',
663
+ height: 'auto',
664
+ width: 'auto',
665
+ minHeight: 200,
666
+ maxHeight: window70vh,
667
+ minWidth: window.innerWidth > 500 ? 500 : initialMaxWidth < window70vw ? initialMaxWidth : window70vw, // minimum width of 500px if possible
668
+ maxWidth: initialMaxWidth,
669
+ display: 'none' // initially hidden
670
+ })
671
+ if (renderMode === 'html') {
672
+ $dialog.html(content) // render as HTML
673
+ } else {
674
+ $dialog.text(content) // render as plain text
675
+ }
676
+ $dialog.appendTo('body')
677
+
678
+ const initialWidth = $dialog.width()
679
+ const width = initialWidth > window70vw ? window70vw : initialWidth
680
+ const initialHeight = $dialog.height() + 40 // +40 for padding/grace etc
681
+ const height = initialHeight > window70vh ? window70vh : initialHeight
682
+
683
+ // now remove the divs min/max so that it sizes to the maximum size of the jquery dialog
684
+ $dialog.css({ maxWidth: '', minWidth: '', maxHeight: '', minHeight: '' })
685
+
686
+ // set the default dialog options
687
+ const defaultOptions = {
688
+ title: title || plugin._('name'),
689
+ modal: true,
690
+ resizable: true,
691
+ width,
692
+ height,
693
+ dialogClass: 'ff-nr-ai-dialog-message',
694
+ closeOnEscape: true,
695
+ draggable: true,
696
+ minHeight: 280,
697
+ minWidth: window.innerWidth > 600 ? 600 : window.innerWidth, // minimum width of 500px if possible
698
+ show: { effect: 'fade', duration: 300 },
699
+ hide: { effect: 'fade', duration: 300 },
700
+ open: function () {
701
+ RED.keyboard.disable()
702
+ },
703
+ close: function () {
704
+ RED.keyboard.enable()
705
+ $(this).dialog('destroy').remove() // clean up
706
+ }
707
+ }
708
+
709
+ // create the dialog with any additional options passed in & return it for later programmatic control
710
+ return $dialog.dialog($.extend({}, defaultOptions, options))
711
+ }
712
+
604
713
  function explainSelectedNodes () {
605
714
  if (!assistantOptions.enabled) {
606
- RED.notify('The FlowFuse Assistant is not enabled', 'warning')
715
+ RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
607
716
  return
608
717
  }
609
718
  const selection = RED.view.selection()
610
719
  if (!selection || !selection.nodes || selection.nodes.length === 0) {
611
- RED.notify(RED._('@flowfuse/nr-assistant/flowfuse-nr-assistant:errors.common.need-1-or-more-nodes-selected'), 'warning')
720
+ RED.notify(plugin._('explain-flows.errors.no-nodes-selected'), 'warning')
612
721
  return
613
722
  }
614
723
  if (selection.nodes.length > 100) { // TODO: increase or make configurable
615
- RED.notify(RED._('@flowfuse/nr-assistant/flowfuse-nr-assistant:errors.common.need-100-or-less-nodes-selected'), 'warning')
724
+ RED.notify(plugin._('explain-flows.errors.too-many-nodes-selected'), 'warning')
616
725
  return
617
726
  }
618
727
 
@@ -636,7 +745,7 @@
636
745
  flowName: '', // FUTURE: include the parent flow name in the context to aid with the explanation
637
746
  userContext: '' // FUTURE: include user textual input context for more personalized explanations
638
747
  }
639
- const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
748
+ const busyNotification = showBusyNotification(plugin._('notifications.busy'), function () {
640
749
  if (xhr) {
641
750
  xhr.abort('abort')
642
751
  xhr = null
@@ -656,44 +765,15 @@
656
765
  }
657
766
 
658
767
  try {
659
- const style = 'success'
660
768
  const text = reply.data || 'No reply from server'
661
-
662
- // FUTURE: Ask for a summary and details in json format and parse it
663
- // let style = 'success'
664
- // try {
665
- // const parsedJson = JSON.parse(reply.data)
666
- // if (!parsedJson.summary && !parsedJson.details) {
667
- // text = 'No summary or details available!'
668
- // style = 'warning'
669
- // }
670
- // const textBuilder = []
671
- // if (parsedJson.summary) {
672
- // textBuilder.push('### Summary')
673
- // textBuilder.push(parsedJson.summary)
674
- // textBuilder.push('')
675
- // }
676
- // if (parsedJson.details) {
677
- // textBuilder.push('### Details')
678
- // textBuilder.push(parsedJson.details)
679
- // textBuilder.push('')
680
- // }
681
- // text = textBuilder.join('\n')
682
- // } catch (error) {
683
- // console.warn('Error parsing reply data', error)
684
- // text = reply.data || 'No data in response from server'
685
- // }
686
- showNotification(
687
- RED.utils.renderMarkdown(text),
688
- { fixed: true, modal: true, timeout: 0, type: style }
689
- )
769
+ showMessage(plugin._('explain-flows.dialog-result.title'), RED.utils.renderMarkdown(text), 'html', {})
690
770
  } catch (error) {
691
771
  console.warn('Error rendering reply', error)
692
772
  showNotification('Sorry, something went wrong, please try again', { type: 'error' })
693
773
  }
694
774
  },
695
775
  error: (jqXHR, textStatus, errorThrown) => {
696
- // console.log('showFunctionBuilderPrompt -> ajax -> error', jqXHR, textStatus, errorThrown)
776
+ // console.log('explainSelectedNodes -> ajax -> error', jqXHR, textStatus, errorThrown)
697
777
  busyNotification.close()
698
778
  if (textStatus === 'abort' || errorThrown === 'abort' || jqXHR.statusText === 'abort') {
699
779
  // user cancelled
@@ -712,7 +792,7 @@
712
792
  // eslint-disable-next-line no-unused-vars
713
793
  function showFlowBuilderPrompt (title) {
714
794
  if (!assistantOptions.enabled) {
715
- RED.notify('The FlowFuse Assistant is not enabled', 'warning')
795
+ RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
716
796
  return
717
797
  }
718
798
  getUserInput({
@@ -734,7 +814,7 @@
734
814
  modulesAllowed
735
815
  }
736
816
  }
737
- const busyNotification = showBusyNotification('Busy processing your request. Please wait...', function () {
817
+ const busyNotification = showBusyNotification(plugin._('notifications.busy'), function () {
738
818
  if (xhr) {
739
819
  xhr.abort('abort')
740
820
  xhr = null
@@ -779,7 +859,7 @@
779
859
  * @returns {{close: () => {}}} - The notification object
780
860
  */
781
861
  function showBusyNotification (message, onCancel, context, poop) {
782
- message = message || 'Busy processing your request. Please wait...'
862
+ message = message || plugin._('notifications.busy')
783
863
  const busyMessage = $('<div>')
784
864
  $('<span>').text(message).appendTo(busyMessage)
785
865
  $('<i>').addClass('fa fa-spinner fa-spin fa-fw').appendTo(busyMessage)
@@ -922,6 +1002,46 @@
922
1002
  }
923
1003
  return bytes.join('').substring(0, length)
924
1004
  }
1005
+
1006
+ /**
1007
+ * Sets a default shortcut key for an action in the specified scope
1008
+ * If the action already has a shortcut key set, it will not be changed.
1009
+ * If the key is already set for another action in the same scope, it will not be set.
1010
+ * @param {string} id - The ID of the menu item (e.g. 'ff-ai-function-builder')
1011
+ * @param {string} scope - The scope of the action (e.g. '*' or 'red-ui-workspace')
1012
+ * @param {string} key - The key to set as the shortcut (e.g. 'ctrl-shift-a') (also supports chords like 'ctrl-a x')
1013
+ * @param {string} action - The action to set the shortcut for (e.g. 'flowfuse-nr-assistant:function-builder')
1014
+ */
1015
+ function setMenuShortcutKey (id, scope, key, action) {
1016
+ console.warn('setMenuShortcutKey')
1017
+ if (!scope || !key || !action) {
1018
+ console.warn('setMenuShortcutKey called with missing parameters', { scope, key, action })
1019
+ return
1020
+ }
1021
+ // first, see if there is already a key set for this action (may have been user set)
1022
+ const hasShortcut = RED.keyboard.getShortcut(action)
1023
+ if (hasShortcut?.key) {
1024
+ debug('setMenuShortcutKey: action already has a shortcut key set', { scope, key, action, hasShortcut })
1025
+ return
1026
+ }
1027
+ // next check if the desired key is already set by scanning the array of RED.actions.list
1028
+ const actionList = RED.actions.list()
1029
+ const existingAction = actionList.find(a => (a.scope === scope || a.scope === '*') && a.key?.toLowerCase() === key.toLowerCase())
1030
+ if (existingAction) {
1031
+ debug('setMenuShortcutKey: key is already set for another action', { scope, key, action, existingAction })
1032
+ return
1033
+ }
1034
+ // set the shortcut key
1035
+ RED.keyboard.add(scope, key, action)
1036
+ // For a menu to show the shortcut key, it needs to have a span with class `red-ui-popover-key` inside the menu item
1037
+ const actionItem = $('#' + id + ' > span > span.red-ui-menu-label')
1038
+ const hasPopoverSpan = actionItem.find('span.red-ui-popover-key').length > 0
1039
+ if (!hasPopoverSpan) {
1040
+ console.warn('setMenuShortcutKey: adding popover span to action item', { id, scope, key, action })
1041
+ actionItem.append('<span class="red-ui-popover-key"></span>')
1042
+ }
1043
+ RED.menu.refreshShortcuts()
1044
+ }
925
1045
  function debug () {
926
1046
  if (RED.nrAssistant?.DEBUG) {
927
1047
  // eslint-disable-next-line no-console
@@ -930,3 +1050,94 @@
930
1050
  }
931
1051
  }(RED, $))
932
1052
  </script>
1053
+
1054
+ <style>
1055
+ /*
1056
+ The node-red styling for width of the menu UL is quite specific and includes !important, so we need to override
1057
+ it with an even more specific selector - this is to prevent it wrapping the text elements to underneath the icon
1058
+ */
1059
+ #red-ui-header ul#red-ui-header-button-ff-ai-submenu.red-ui-menu-dropdown {
1060
+ width: 300px !important; /* make the submenu wider to prevent wrapping elements */
1061
+ }
1062
+ #red-ui-header ul.red-ui-menu-dropdown li a {
1063
+ padding: 8px 16px;
1064
+ }
1065
+
1066
+ /* prevent icon color being set to background color when clicked */
1067
+ a#red-ui-header-button-ff-ai.button:active, a#red-ui-header-button-ff-ai.button.active {
1068
+ background-color: var(--red-ui-header-menu-sublabel-color);
1069
+ }
1070
+ a#red-ui-header-button-ff-ai.button:hover {
1071
+ /* Override the background color when clicked to use this var - prevent it going dark */
1072
+ background-color: #8CE2E7;
1073
+ }
1074
+
1075
+ #red-ui-header-button-ff-ai-submenu a {
1076
+ display: flex;
1077
+ }
1078
+
1079
+ #red-ui-header-button-ff-ai-submenu .red-ui-menu-label-container {
1080
+ flex-grow: 1;
1081
+ }
1082
+
1083
+ /*
1084
+ As menu icons are drawn by the (RED.menu.init api) as <img> tags, styling the fill of the path is impossible.
1085
+ Instead of using a url (e.g. resource/icon.svg), we add them as a "class name" which node-red then renders as an `<i>` tag.
1086
+ The CSS below uses the mask-image property to apply the SVG as a mask, allowing us to set the color via background-color.
1087
+ */
1088
+ #red-ui-header-button-ff-ai {
1089
+ mask-image: url("resources/@flowfuse/nr-assistant/ff-assistant.svg");
1090
+ mask-repeat: no-repeat;
1091
+ mask-position: center;
1092
+ background-color: currentColor;
1093
+ height: 30px;
1094
+ }
1095
+ i.ff-assistant-menu-icon {
1096
+ display: inline-block;
1097
+ width: 28px;
1098
+ height: 28px;
1099
+ padding: 4px;
1100
+ margin-right: 10px;
1101
+ background-color: var(--red-ui-header-menu-color);
1102
+
1103
+ /* Use mask-image to apply the SVG shape */
1104
+ mask-repeat: no-repeat;
1105
+ mask-position: center;
1106
+ mask-size: contain; /* alt 'cover' */
1107
+ -webkit-mask-repeat: no-repeat;
1108
+ -webkit-mask-position: center;
1109
+ -webkit-mask-size: contain;
1110
+ }
1111
+ i.ff-assistant-menu-icon.function {
1112
+ mask-image: url('resources/@flowfuse/nr-assistant/ff-assistant-function.svg');
1113
+ -webkit-mask-image: url('resources/@flowfuse/nr-assistant/ff-assistant-function.svg');
1114
+ }
1115
+ i.ff-assistant-menu-icon.explain-flows {
1116
+ mask-image: url('resources/@flowfuse/nr-assistant/ff-assistant-explain-flows.svg');
1117
+ -webkit-mask-image: url('resources/@flowfuse/nr-assistant/ff-assistant-explain-flows.svg');
1118
+ }
1119
+
1120
+ /* Header title in the dropdown menu */
1121
+ #ff-assistant-title > span.red-ui-menu-label > span {
1122
+ color: var(--red-ui-header-menu-color);
1123
+ font-size: 16px;
1124
+ display: inline-block;
1125
+ text-indent: 0px;
1126
+ margin-top: 10px;
1127
+ font-weight: bold;
1128
+ }
1129
+
1130
+ /* styling for showMessage dialog */
1131
+ .ff-nr-ai-dialog-message > div.ui-dialog-content {
1132
+ padding: 16px;
1133
+ }
1134
+ .ff-nr-ai-dialog-message h3 {
1135
+ margin: 0 0 12px;
1136
+ }
1137
+ .ff-nr-ai-dialog-message p, li {
1138
+ line-height: 1.25rem;
1139
+ }
1140
+ .ff-nr-ai-dialog-message li {
1141
+ margin-bottom: 3px;
1142
+ }
1143
+ </style>
@@ -1,13 +1,40 @@
1
1
  {
2
2
  "name": "FlowFuse Assistant",
3
- "actions": {
4
- "function-builder": "FlowFuse Assistant: Create a Function Node",
5
- "explain-selected-nodes": "FlowFuse Assistant: Explain Selected Nodes"
3
+ "function-builder": {
4
+ "action": {
5
+ "label": "FlowFuse Assistant: Create a Function Node"
6
+ },
7
+ "menu": {
8
+ "label": "Function Builder",
9
+ "description": "Create a Function Node"
10
+ },
11
+ "dialog-input": {
12
+ "title": "FlowFuse Assistant: Create a Function Node",
13
+ "explanation": "The FlowFuse Assistant can help you create a Function Node.",
14
+ "description": "Enter a short description of what you want it to do.",
15
+ "create-button": "Ask the FlowFuse Assistant 🪄"
16
+ }
6
17
  },
7
- "errors": {
8
- "common": {
9
- "need-1-or-more-nodes-selected": "Please select 1 node or more nodes.",
10
- "need-100-or-less-nodes-selected": "Please select less than 100 nodes."
18
+ "explain-flows": {
19
+ "action": {
20
+ "label": "FlowFuse Assistant: Explain Selected Nodes"
21
+ },
22
+ "menu": {
23
+ "label": "Explain Flows",
24
+ "description": "Explain the selected nodes"
25
+ },
26
+ "dialog-result": {
27
+ "title": "FlowFuse Assistant: Explain Flows"
28
+ },
29
+ "errors": {
30
+ "no-nodes-selected": "No nodes selected. Please select one or more nodes to explain.",
31
+ "too-many-nodes-selected": "Too many nodes selected. Please select less than 100 nodes to explain."
11
32
  }
33
+ },
34
+ "notifications": {
35
+ "busy": "Busy processing your request. Please wait..."
36
+ },
37
+ "errors":{
38
+ "assistant-not-enabled": "The FlowFuse Assistant is not enabled"
12
39
  }
13
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/nr-assistant",
3
- "version": "0.2.2-28e5c9f-202506121535.0",
3
+ "version": "0.2.2-480c08b-202506180948.0",
4
4
  "description": "FlowFuse Node-RED assistant plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,7 @@
1
+ <svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3.38853 13.6317C3.83835 13.6317 4.20821 13.992 4.20821 14.4424V21.3183H11.3454C12.6249 21.3183 13.7145 22.2291 13.8344 23.4802C14.7841 23.3601 15.3539 23.0998 15.7637 22.7195C16.2535 22.2691 16.5834 21.6386 16.9932 20.8979C17.4031 20.1573 17.9029 19.3066 18.8125 18.686C19.5422 18.1756 20.6318 17.9354 21.9013 17.8653V17.425C21.9013 16.0638 22.9609 14.9628 24.3204 14.9628H30.7079V5.24449C30.7079 4.49385 30.0981 3.87332 29.3384 3.87332H14.4942C14.0444 3.87332 13.6745 3.51301 13.6745 3.06262C13.6745 2.61223 14.0344 2.25192 14.4942 2.25192H29.3384C30.9977 2.25192 32.3472 3.60308 32.3472 5.2545V29.0149C32.3472 30.6763 30.9977 32.0175 29.3384 32.0175H5.57767C3.91832 32.0175 2.56885 30.6663 2.56885 29.0149V14.4524C2.56885 14.002 2.92871 13.6417 3.38853 13.6417V13.6317ZM12.1351 23.7204C12.1351 23.26 11.7952 22.8796 11.3354 22.8796H4.20821V26.9031H11.3354C11.7952 26.9031 12.1351 26.6028 12.1351 26.1324V23.7104V23.7204ZM24.3204 16.6743C23.8605 16.6743 23.4807 16.9646 23.4807 17.425V19.847C23.4807 20.3074 23.8505 20.6177 24.3104 20.6177H30.6979V16.6643H24.3104L24.3204 16.6743ZM5.57767 30.3861H29.3284C30.0881 30.3861 30.6979 29.7756 30.6979 29.0149V22.3292H24.3104C22.9509 22.3292 21.8913 21.2182 21.8913 19.8571V19.5468C20.9317 19.5968 20.182 19.777 19.7521 20.0772C19.1924 20.4676 18.8425 21.0181 18.4526 21.7187C18.0928 22.3692 17.6929 23.1399 17.0232 23.8104C16.9532 23.8905 16.4134 24.481 15.6937 24.8213C15.014 25.1516 13.8744 25.2217 13.8744 25.2217V26.1425C13.8744 27.5036 12.6949 28.6146 11.3354 28.6146H4.19821V29.0249C4.19821 29.7756 4.80797 30.3961 5.56768 30.3961L5.57767 30.3861Z" fill="black"/>
3
+ <path d="M0.669786 5.41464L3.23879 4.46382C3.9685 4.19359 4.53828 3.6231 4.80817 2.90248L5.7578 0.340279C5.91774 -0.0900906 6.5275 -0.0900906 6.67744 0.340279L7.62707 2.90248C7.89697 3.63311 8.46674 4.2036 9.19646 4.46382L11.7655 5.41464C12.1953 5.57478 12.1953 6.1853 11.7655 6.33543L9.19646 7.28625C8.46674 7.55648 7.89697 8.12697 7.62707 8.84759L6.67744 11.4098C6.5175 11.8402 5.90774 11.8402 5.7578 11.4098L4.80817 8.84759C4.53828 8.11696 3.9685 7.54647 3.23879 7.28625L0.669786 6.33543C0.239954 6.17529 0.239954 5.56477 0.669786 5.41464Z" fill="black"/>
4
+ <path d="M17.4133 16.6643C17.1634 16.6643 16.9135 16.5742 16.7036 16.4041L15.1142 15.0729H14.1046C13.185 15.0729 12.4253 14.3223 12.4253 13.4015V9.48813C12.4253 8.56734 13.175 7.8167 14.1046 7.8167H20.782C21.7017 7.8167 22.4614 8.56734 22.4614 9.48813V13.4015C22.4614 14.3223 21.7117 15.0729 20.782 15.0729H19.7124L18.1231 16.4041C17.9132 16.5742 17.6632 16.6643 17.4133 16.6643ZM14.1146 8.82756C13.7448 8.82756 13.4449 9.12782 13.4449 9.49814V13.4115C13.4449 13.7818 13.7448 14.0821 14.1146 14.0821H15.3042C15.4241 14.0821 15.5341 14.1221 15.624 14.2022L17.3534 15.6534C17.3534 15.6534 17.4433 15.6835 17.4733 15.6534L19.2026 14.2022C19.2926 14.1221 19.4126 14.0821 19.5225 14.0821H20.772C21.1419 14.0821 21.4418 13.7818 21.4418 13.4115V9.49814C21.4418 9.12782 21.1419 8.82756 20.772 8.82756H14.0946H14.1146Z" fill="black"/>
5
+ <path d="M20.342 11.1387H14.5542C14.2744 11.1387 14.0544 10.9087 14.0544 10.6387C14.0544 10.3687 14.2844 10.1387 14.5542 10.1387H20.342C20.6219 10.1387 20.8418 10.3687 20.8418 10.6387C20.8418 10.9087 20.6119 11.1387 20.342 11.1387Z" fill="black"/>
6
+ <path d="M20.342 12.7601H14.5542C14.2744 12.7601 14.0544 12.5301 14.0544 12.2601C14.0544 11.9901 14.2844 11.7601 14.5542 11.7601H20.342C20.6219 11.7601 20.8418 11.9901 20.8418 12.2601C20.8418 12.5301 20.6119 12.7601 20.342 12.7601Z" fill="black"/>
7
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4.4425 8.83208L5.3925 11.3927C5.5525 11.8228 6.1625 11.8228 6.3125 11.3927L7.2625 8.83208C7.5325 8.1019 8.1025 7.53177 8.8225 7.27171L11.3825 6.32149C11.8125 6.16145 11.8125 5.56131 11.3825 5.40127L8.8225 4.45105C8.0925 4.18098 7.5225 3.61085 7.2625 2.89068L6.3125 0.330083C6.1525 -0.100018 5.5425 -0.100018 5.3925 0.330083L4.4425 2.89068C4.1725 3.62085 3.6025 4.19099 2.8825 4.45105L0.3225 5.40127C-0.1075 5.56131 -0.1075 6.16145 0.3225 6.32149L2.8825 7.27171C3.6125 7.54177 4.1825 8.11191 4.4425 8.83208Z" fill="black"/>
3
+ <path d="M10.2524 8.71205L10.8224 9.95234C10.9824 10.3024 10.9824 10.7025 10.8224 11.0626L10.2524 12.3029C10.1524 12.5129 10.3724 12.723 10.5824 12.633L11.8224 12.0628C12.1724 11.9028 12.5724 11.9028 12.9324 12.0628L14.1724 12.633C14.3824 12.733 14.5924 12.5129 14.5024 12.3029L13.9324 11.0626C13.7724 10.7125 13.7724 10.3124 13.9324 9.95234L14.5024 8.71205C14.6024 8.502 14.3824 8.29195 14.1724 8.38197L12.9324 8.9521C12.5824 9.11214 12.1824 9.11214 11.8224 8.9521L10.5824 8.38197C10.3724 8.28195 10.1624 8.502 10.2524 8.71205Z" fill="black"/>
4
+ <path d="M29.0124 32.0075H5.21242C3.57242 32.0075 2.23242 30.6672 2.23242 29.0268V14.4134C2.23242 13.9733 2.59242 13.6032 3.04242 13.6032C3.49242 13.6032 3.85242 13.9633 3.85242 14.4134V29.0268C3.85242 29.787 4.47242 30.3971 5.22242 30.3971H29.0224C29.7824 30.3971 30.3924 29.787 30.3924 29.0268V5.25124C30.3924 4.49107 29.7724 3.88092 29.0224 3.88092H14.1424C13.6924 3.88092 13.3324 3.52084 13.3324 3.07073C13.3324 2.62063 13.6924 2.26054 14.1424 2.26054H29.0224C30.6624 2.26054 32.0024 3.60086 32.0024 5.24124V29.0068C32.0024 30.6472 30.6624 31.9875 29.0224 31.9875L29.0124 32.0075Z" fill="black"/>
5
+ <path d="M27.7824 14.2933C27.3024 13.8132 26.3924 13.7732 26.0524 13.7732C25.4524 13.7732 24.6624 13.9233 24.2824 14.3134C23.7124 14.8935 23.4924 15.9337 23.3924 16.4338L23.3724 16.5439C23.2324 17.6041 23.1524 18.6844 23.0624 19.7246C23.0324 20.0647 23.0124 20.3948 22.9824 20.7349H20.4124C20.2724 20.7349 20.1624 20.8449 20.1624 20.9849V21.9651C20.1624 22.1052 20.2724 22.2152 20.4124 22.2152H22.7824C22.7024 23.4755 22.5124 26.0861 22.4224 26.4662L22.4024 26.5562C22.2724 27.1664 22.1924 27.3264 21.8424 27.2764C21.4124 27.2264 21.4224 26.5262 21.4224 26.5262C21.4224 26.4562 21.3924 26.3962 21.3524 26.3462C21.3024 26.2962 21.2424 26.2662 21.1724 26.2662H19.5924C19.5224 26.2662 19.4424 26.3062 19.4024 26.3662C19.3524 26.4262 19.3324 26.4962 19.3524 26.5762C19.3624 26.6362 19.6324 27.9565 20.3324 28.6567C20.6624 28.9868 21.2824 29.1768 22.0324 29.1768H22.0624C22.6724 29.1768 23.4524 29.0268 23.8324 28.6367C24.4024 28.0566 24.6224 27.0163 24.7224 26.5162L24.7424 26.4062C24.8824 25.3459 24.9624 24.2657 25.0524 23.2254C25.0824 22.8854 25.1024 22.5553 25.1324 22.2152H27.2624C27.4024 22.2152 27.5124 22.1052 27.5124 21.9651V20.9849C27.5124 20.8449 27.4024 20.7349 27.2624 20.7349H25.3324C25.4124 19.4746 25.6024 16.8639 25.6924 16.4839L25.7124 16.3938C25.8424 15.7837 25.9224 15.6337 26.2724 15.6737C26.7024 15.7237 26.6924 16.4238 26.6924 16.4338C26.6924 16.5039 26.7224 16.5639 26.7624 16.6139C26.8124 16.6639 26.8724 16.6939 26.9424 16.6939H28.5224C28.5924 16.6939 28.6724 16.6539 28.7124 16.5939C28.7624 16.5339 28.7824 16.4639 28.7624 16.3838C28.7524 16.3238 28.4824 15.0035 27.7924 14.3033L27.7824 14.2933Z" fill="black"/>
6
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4.45111 8.83208L5.40133 11.3927C5.56137 11.8228 6.17151 11.8228 6.32155 11.3927L7.27177 8.83208C7.54183 8.1019 8.11197 7.53177 8.83214 7.27171L11.3927 6.32149C11.8228 6.16145 11.8228 5.56131 11.3927 5.40127L8.83214 4.45105C8.10197 4.18098 7.53183 3.61085 7.27177 2.89068L6.32155 0.330083C6.16151 -0.100018 5.55137 -0.100018 5.40133 0.330083L4.45111 2.89068C4.18105 3.62085 3.61091 4.19099 2.89074 4.45105L0.330144 5.40127C-0.0999569 5.56131 -0.0999569 6.16145 0.330144 6.32149L2.89074 7.27171C3.62092 7.54177 4.19105 8.11191 4.45111 8.83208Z" fill="black"/>
3
+ <path d="M10.2524 8.71205L10.8226 9.95234C10.9826 10.3024 10.9826 10.7025 10.8226 11.0626L10.2524 12.3029C10.1524 12.5129 10.3725 12.723 10.5825 12.633L11.8228 12.0628C12.1729 11.9028 12.573 11.9028 12.9331 12.0628L14.1733 12.633C14.3834 12.733 14.5934 12.5129 14.5034 12.3029L13.9333 11.0626C13.7732 10.7125 13.7732 10.3124 13.9333 9.95234L14.5034 8.71205C14.6034 8.502 14.3834 8.29195 14.1733 8.38197L12.9331 8.9521C12.583 9.11214 12.1829 9.11214 11.8228 8.9521L10.5825 8.38197C10.3725 8.28195 10.1624 8.502 10.2524 8.71205Z" fill="black"/>
4
+ <path d="M29.0167 32.0075H5.21117C3.57078 32.0075 2.23047 30.6672 2.23047 29.0268V14.4134C2.23047 13.9733 2.59055 13.6032 3.04066 13.6032C3.49076 13.6032 3.85085 13.9633 3.85085 14.4134V29.0268C3.85085 29.787 4.47099 30.3971 5.22117 30.3971H29.0268C29.7869 30.3971 30.3971 29.787 30.3971 29.0268V5.25124C30.3971 4.49107 29.7769 3.88092 29.0268 3.88092H14.1433C13.6932 3.88092 13.3331 3.52084 13.3331 3.07073C13.3331 2.62063 13.6932 2.26054 14.1433 2.26054H29.0268C30.6671 2.26054 32.0074 3.60086 32.0074 5.24124V29.0068C32.0074 30.6472 30.6671 31.9875 29.0268 31.9875L29.0167 32.0075Z" fill="black"/>
5
+ <path d="M17.7243 25.546L16.9441 28.3667H14.3735L17.7343 17.3641H20.9951L24.4259 28.3667H21.7453L20.8951 25.546H17.7343H17.7243ZM20.525 23.6956L19.8348 21.355C19.6448 20.7049 19.4547 19.8747 19.2847 19.2345H19.2547C19.0946 19.8847 18.9246 20.7149 18.7546 21.355L18.0944 23.6856H20.535L20.525 23.6956Z" fill="black"/>
6
+ <path d="M28.3762 17.3741V28.3767H25.8862V17.3741H28.3762Z" fill="black"/>
7
+ </svg>