Skip to content

Commit 429f18d

Browse files
authored
Merge pull request #72 from FlowFuse/71-tables-codelens
Add tables codelens feature
2 parents 91d3033 + 824af26 commit 429f18d

File tree

5 files changed

+207
-13
lines changed

5 files changed

+207
-13
lines changed

index.html

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
const modulesAllowed = RED.settings.functionExternalModules !== false
2525
const assistantOptions = {
2626
enabled: false,
27+
tablesEnabled: false,
2728
requestTimeout: AI_TIMEOUT
2829
}
2930
let assistantInitialised = false
@@ -42,6 +43,7 @@
4243
if (topic === 'nr-assistant/initialise') {
4344
assistantOptions.enabled = !!msg?.enabled
4445
assistantOptions.requestTimeout = msg?.requestTimeout || AI_TIMEOUT
46+
assistantOptions.tablesEnabled = msg?.tablesEnabled === true
4547
initAssistant(msg)
4648
RED.actions.add('flowfuse-nr-assistant:function-builder', showFunctionBuilderPrompt, { label: '@flowfuse/nr-assistant/flowfuse-nr-assistant:function-builder.action.label' })
4749
setMenuShortcutKey('ff-assistant-function-builder', 'red-ui-workspace', 'ctrl-alt-f', 'flowfuse-nr-assistant:function-builder')
@@ -69,7 +71,7 @@
6971
}
7072
RED.plugins.registerPlugin('flowfuse-nr-assistant', plugin)
7173

72-
function initAssistant () {
74+
function initAssistant (options) {
7375
if (assistantInitialised) {
7476
return
7577
}
@@ -88,6 +90,7 @@
8890
const jsonCommandId = 'nr-assistant-json-inline'
8991
const cssCommandId = 'nr-assistant-css-inline'
9092
const db2uiTemplateCommandId = 'nr-assistant-html-dashboard2-template-inline'
93+
const ffTablesNodeCommandId = 'nr-assistant-ff-tables-node-inline'
9194

9295
debug('registering code lens providers...')
9396

@@ -264,6 +267,48 @@
264267
}
265268
})
266269

270+
assistantOptions.tablesEnabled && monaco.languages.registerCodeLensProvider('sql', {
271+
provideCodeLenses: function (model, token) {
272+
debug('SQL CodeLens provider called', model, token)
273+
const thisEditor = getMonacoEditorForModel(model)
274+
if (!thisEditor) {
275+
return
276+
}
277+
const node = RED.view.selection()?.nodes?.[0]
278+
// only support tables query nodes for now
279+
if (!node || node.type !== 'tables-query' || node._def?.set?.id !== '@flowfuse/nr-tables-nodes/tables-query') {
280+
return
281+
}
282+
return {
283+
lenses: [
284+
{
285+
range: {
286+
startLineNumber: 1,
287+
startColumn: 1,
288+
endLineNumber: 2,
289+
endColumn: 1
290+
},
291+
id: ffTablesNodeCommandId
292+
}
293+
],
294+
dispose: () => { }
295+
}
296+
},
297+
resolveCodeLens: function (model, codeLens, token) {
298+
debug('SQL CodeLens resolve called', model, codeLens, token)
299+
if (codeLens.id !== ffTablesNodeCommandId) {
300+
return codeLens
301+
}
302+
codeLens.command = {
303+
id: codeLens.id,
304+
title: 'Ask the FlowFuse Assistant 🪄',
305+
tooltip: 'Click to ask FlowFuse Assistant for help with PostgreSQL',
306+
arguments: [model, codeLens, token]
307+
}
308+
return codeLens
309+
}
310+
})
311+
267312
debug('registering commands...')
268313

269314
monaco.editor.registerCommand(funcCommandId, function (accessor, model, codeLens, token) {
@@ -581,6 +626,69 @@
581626
}
582627
})
583628

629+
assistantOptions.tablesEnabled && monaco.editor.registerCommand(ffTablesNodeCommandId, function (accessor, model, codeLens, token) {
630+
debug('running command', ffTablesNodeCommandId)
631+
const node = RED.view.selection()?.nodes?.[0]
632+
if (!node) {
633+
console.warn('No node selected') // should not happen
634+
return
635+
}
636+
if (!assistantOptions.enabled) {
637+
RED.notify(plugin._('errors.assistant-not-enabled'), 'warning')
638+
return
639+
}
640+
const thisEditor = getMonacoEditorForModel(model)
641+
if (thisEditor) {
642+
if (!document.body.contains(thisEditor.getDomNode())) {
643+
console.warn('Editor is no longer in the DOM, cannot proceed.')
644+
return
645+
}
646+
647+
// FUTURE: for including selected text in the context for features like "fix my code", "refactor this", "what is this?" etc
648+
// const userSelection = triggeredEditor.getSelection()
649+
// const selectedText = model.getValueInRange(userSelection)
650+
/** @type {PromptOptions} */
651+
const promptOptions = {
652+
method: 'flowfuse-tables-query',
653+
lang: 'sql',
654+
dialect: 'g',
655+
type: node.type
656+
// selectedText: model.getValueInRange(userSelection)
657+
}
658+
/** @type {PromptUIOptions} */
659+
const uiOptions = {
660+
title: 'FlowFuse Assistant : FlowFuse Query',
661+
explanation: 'The FlowFuse Assistant can help you write SQL queries.',
662+
description: 'Enter a short description of what you want it to do.'
663+
}
664+
doPrompt(node, thisEditor, promptOptions, uiOptions, (error, response) => {
665+
if (error) {
666+
console.warn('Error processing request', error)
667+
return
668+
}
669+
debug('sql response', response)
670+
const responseData = response?.data
671+
if (responseData && responseData.sql) {
672+
// ensure the editor is still present in the DOM
673+
if (!document.body.contains(thisEditor.getDomNode())) {
674+
console.warn('Editor is no longer in the DOM')
675+
return
676+
}
677+
thisEditor.focus()
678+
const currentSelection = thisEditor.getSelection()
679+
thisEditor.executeEdits('', [
680+
{
681+
range: new monaco.Range(currentSelection.startLineNumber, currentSelection.startColumn, currentSelection.endLineNumber, currentSelection.endColumn),
682+
text: responseData.sql
683+
}
684+
])
685+
}
686+
})
687+
} else {
688+
console.warn('Could not find editor for model', model.uri.toString())
689+
}
690+
})
691+
584692
const toolbarMenuButton = $('<li><a id="red-ui-header-button-ff-ai" class="button" href="#"></a></li>')
585693
const toolbarMenuButtonAnchor = toolbarMenuButton.find('a')
586694
const deployButtonLi = $('#red-ui-header-button-deploy').closest('li')

lib/assistant.js

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,12 @@ const { getLongestUpstreamPath } = require('./flowGraph')
66
const { hasProperty } = require('./utils')
77
const semver = require('semver')
88

9-
const FF_ASSISTANT_USER_AGENT = 'FlowFuse Assistant Plugin/' + require('../package.json').version
10-
9+
// import typedef AssistantSettings
1110
/**
12-
* @typedef {Object} AssistantSettings
13-
* @property {boolean} enabled - Whether the Assistant is enabled
14-
* @property {number} requestTimeout - The timeout for requests to the Assistant backend in milliseconds
15-
* @property {string} url - The URL of the Assistant server
16-
* @property {string} token - The authentication token for the Assistant server
17-
* @property {Object} [got] - The got instance to use for HTTP requests
18-
* @property {Object} completions - Settings for completions
19-
* @property {string} completions.modelUrl - The URL to the ML model
20-
* @property {string} completions.vocabularyUrl - The URL to the completions vocabulary lookup data
11+
* @typedef {import('./settings.js').AssistantSettings} AssistantSettings
2112
*/
2213

14+
const FF_ASSISTANT_USER_AGENT = 'FlowFuse Assistant Plugin/' + require('../package.json').version
2315
class Assistant {
2416
constructor () {
2517
// Main properties
@@ -70,7 +62,7 @@ class Assistant {
7062
await this.dispose() // Dispose of any existing instance before initializing a new one
7163
this.RED = RED
7264
this.options = options || {}
73-
this.got = this.options.got || require('got') // got can me passed in for testing purposes
65+
this.got = this.options.got || require('got') // got can be passed in for testing purposes
7466

7567
if (!this.options.enabled) {
7668
RED.log.info('FlowFuse Assistant Plugin is not enabled')
@@ -88,6 +80,7 @@ class Assistant {
8880

8981
const clientSettings = {
9082
enabled: this.options.enabled !== false && !!this.options.url,
83+
tablesEnabled: this.options.tables?.enabled === true,
9184
requestTimeout: this.options.requestTimeout || 60000
9285
}
9386
RED.comms.publish('nr-assistant/initialise', clientSettings, true /* retain */)

lib/settings.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
1+
/**
2+
* @typedef {Object} AssistantSettings
3+
* @property {boolean} enabled - Whether the Assistant is enabled
4+
* @property {number} requestTimeout - The timeout for requests to the Assistant backend in milliseconds
5+
* @property {string} url - The URL of the Assistant server
6+
* @property {string} token - The authentication token for the Assistant server
7+
* @property {Object} [got] - The got instance to use for HTTP requests
8+
* @property {Object} completions - Settings for completions
9+
* @property {string} completions.modelUrl - The URL to the ML model
10+
* @property {string} completions.vocabularyUrl - The URL to the completions vocabulary lookup data
11+
* @property {Object} tables - Settings for tables
12+
* @property {Boolean} tables.enabled - Whether the tables feature is enabled
13+
*/
14+
115
module.exports = {
16+
/**
17+
* Get the Assistant settings from the RED instance.
18+
* @param {Object} RED - The RED instance
19+
* @returns {AssistantSettings} - The Assistant settings
20+
*/
221
getSettings: (RED) => {
322
const assistantSettings = (RED.settings.flowforge && RED.settings.flowforge.assistant) || {}
423
if (assistantSettings.enabled !== true) {
@@ -13,6 +32,9 @@ module.exports = {
1332
modelUrl: null,
1433
vocabularyUrl: null
1534
}
35+
assistantSettings.tables = {
36+
enabled: !!(RED.settings.flowforge?.tables?.token) // for MVP, use the presence of a token is an indicator that tables are enabled
37+
}
1638
return assistantSettings
1739
}
1840
}

test/unit/lib/assistant.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const RED = {
3131
},
3232
settings: {
3333
flowforge: {
34+
tables: {
35+
token: 'test-token'
36+
},
3437
assistant: {
3538
enabled: true,
3639
url: 'http://localhost:8080/assistant',
@@ -198,12 +201,14 @@ describe('assistant', () => {
198201
RED.comms.publish.firstCall.args[0].should.equal('nr-assistant/initialise')
199202
RED.comms.publish.firstCall.args[1].should.eql({
200203
enabled: true,
204+
tablesEnabled: false,
201205
requestTimeout: 60000
202206
})
203207

204208
RED.comms.publish.secondCall.args[0].should.equal('nr-assistant/mcp/ready')
205209
RED.comms.publish.secondCall.args[1].should.eql({
206210
enabled: true,
211+
tablesEnabled: false,
207212
requestTimeout: 60000
208213
})
209214

test/unit/settings.test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/// <reference types="should" />
2+
'use strict'
3+
4+
const should = require('should')
5+
const settings = require('../../lib/settings')
6+
7+
describe('settings', function () {
8+
let RED
9+
10+
beforeEach(function () {
11+
RED = { settings: { flowforge: {} } }
12+
})
13+
14+
describe('getSettings', function () {
15+
it('should be disabled if assistant is not enabled', function () {
16+
RED.settings.flowforge.assistant = { enabled: false }
17+
const result = settings.getSettings(RED)
18+
result.enabled.should.be.false()
19+
})
20+
21+
it('should be enabled with defaults', function () {
22+
RED.settings.flowforge.assistant = { enabled: true }
23+
const result = settings.getSettings(RED)
24+
result.enabled.should.be.true()
25+
result.completions.should.be.an.Object()
26+
result.completions.enabled.should.be.true()
27+
should(result.completions.modelUrl).be.null()
28+
should(result.completions.vocabularyUrl).be.null()
29+
})
30+
31+
it('should preserve completions if provided and enabled', function () {
32+
RED.settings.flowforge.assistant = {
33+
enabled: true,
34+
completions: {
35+
enabled: false,
36+
modelUrl: 'http://model',
37+
vocabularyUrl: 'http://vocab'
38+
}
39+
}
40+
const result = settings.getSettings(RED)
41+
result.completions.enabled.should.be.false()
42+
result.completions.modelUrl.should.equal('http://model')
43+
result.completions.vocabularyUrl.should.equal('http://vocab')
44+
})
45+
46+
it('should set tables.enabled true if tables token exists', function () {
47+
RED.settings.flowforge.tables = { token: 'abc' }
48+
RED.settings.flowforge.assistant = { enabled: true }
49+
const result = settings.getSettings(RED)
50+
result.tables.enabled.should.be.true()
51+
})
52+
53+
it('should set mcp.enabled true by default', function () {
54+
RED.settings.flowforge.assistant = { enabled: true }
55+
const result = settings.getSettings(RED)
56+
result.mcp.should.be.an.Object()
57+
result.mcp.enabled.should.be.true()
58+
})
59+
60+
it('should not throw if flowforge or assistant is missing', function () {
61+
RED = { settings: {} }
62+
const result = settings.getSettings(RED)
63+
result.enabled.should.be.false()
64+
})
65+
})
66+
})

0 commit comments

Comments
 (0)