@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
|
-
|
|
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-
|
|
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('
|
|
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('
|
|
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('
|
|
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.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 || '
|
|
413
|
-
explanation: uiOptions?.explanation || '
|
|
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('
|
|
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: '
|
|
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
|
-
|
|
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('
|
|
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: '
|
|
549
|
-
description: '
|
|
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('
|
|
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('
|
|
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(
|
|
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(
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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 || '
|
|
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>
|
package/locales/en-US/index.json
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "FlowFuse Assistant",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
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
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
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
|
@@ -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>
|