From 32eb3248d41feb58be055f27e3e86213d750310c Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Mon, 17 Jul 2023 14:26:44 +0200 Subject: [PATCH 001/116] feat(editor): Ask AI tab and CLi connection Signed-off-by: Oleg Ivaniv --- packages/cli/src/Server.ts | 2 + packages/cli/src/controllers/ai.controller.ts | 41 +++++++ packages/cli/src/controllers/index.ts | 1 + packages/editor-ui/src/api/ai.ts | 13 ++ .../src/components/CodeNodeEditor/AskAI.vue | 114 ++++++++++++++++++ .../CodeNodeEditor/CodeNodeEditor.vue | 84 +++++++++++-- .../src/components/RunDataSchema.vue | 17 +-- packages/editor-ui/src/composables/index.ts | 1 + .../src/composables/useDataSchema.ts | 84 +++++++++++++ .../src/plugins/i18n/locales/en.json | 9 ++ 10 files changed, 345 insertions(+), 21 deletions(-) create mode 100644 packages/cli/src/controllers/ai.controller.ts create mode 100644 packages/editor-ui/src/api/ai.ts create mode 100644 packages/editor-ui/src/components/CodeNodeEditor/AskAI.vue create mode 100644 packages/editor-ui/src/composables/useDataSchema.ts diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 9f0ec4c58b6e6..2da5c6f80fe56 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -97,6 +97,7 @@ import { TagsController, TranslationController, UsersController, + AiController, } from '@/controllers'; import { executionsController } from '@/executions/executions.controller'; @@ -502,6 +503,7 @@ export class Server extends AbstractServer { }), new SamlController(samlService), new SourceControlController(sourceControlService, sourceControlPreferencesService), + new AiController(), ]; if (isLdapEnabled()) { diff --git a/packages/cli/src/controllers/ai.controller.ts b/packages/cli/src/controllers/ai.controller.ts new file mode 100644 index 0000000000000..3584f965241aa --- /dev/null +++ b/packages/cli/src/controllers/ai.controller.ts @@ -0,0 +1,41 @@ +import type { Request } from 'express'; +import { Authorized, Post, RestController } from '@/decorators'; +import type { IDataObject } from 'n8n-workflow'; +import { Script } from 'vm'; + +type GenerateCodeRequest = Request< + {}, + {}, + { + prompt: string; + schema: IDataObject; + } +>; + +@Authorized() +@RestController('/ai') +export class AiController { + @Post('/generate-code') + async generatePrompt(req: GenerateCodeRequest) { + console.log('Received: ', req.body.prompt, req.body.schema); + const generatedCode = 'console.log("Hello world!")'; + + return { + code: generatedCode, + isValid: this.validateGeneratedCode(generatedCode), + }; + } + + // Validates the generated code by setting it up in a sandbox + // this doesn't execute it, just checks if it's valid + validateGeneratedCode(code: string) { + try { + new Script(code); + + return true; + } catch (error) { + console.log('Generated code is invalid: ', error); + return false; + } + } +} diff --git a/packages/cli/src/controllers/index.ts b/packages/cli/src/controllers/index.ts index 04b46af5f13df..b7d89731d3ec4 100644 --- a/packages/cli/src/controllers/index.ts +++ b/packages/cli/src/controllers/index.ts @@ -8,3 +8,4 @@ export { PasswordResetController } from './passwordReset.controller'; export { TagsController } from './tags.controller'; export { TranslationController } from './translation.controller'; export { UsersController } from './users.controller'; +export { AiController } from './ai.controller'; diff --git a/packages/editor-ui/src/api/ai.ts b/packages/editor-ui/src/api/ai.ts new file mode 100644 index 0000000000000..237b7d975e4d5 --- /dev/null +++ b/packages/editor-ui/src/api/ai.ts @@ -0,0 +1,13 @@ +import type { IRestApiContext } from '@/Interface'; +import { makeRestApiRequest } from '@/utils/apiUtils'; +import type { IDataObject } from 'n8n-workflow'; + +export async function generateCodeForPrompt( + context: IRestApiContext, + { prompt, schema }: { prompt: string; schema: IDataObject }, +): Promise<{ code: string }> { + return makeRestApiRequest(context, 'POST', '/ai/generate-code', { + prompt, + schema: JSON.stringify(schema), + } as IDataObject); +} diff --git a/packages/editor-ui/src/components/CodeNodeEditor/AskAI.vue b/packages/editor-ui/src/components/CodeNodeEditor/AskAI.vue new file mode 100644 index 0000000000000..3cf6c0c4918f6 --- /dev/null +++ b/packages/editor-ui/src/components/CodeNodeEditor/AskAI.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 9deed663d1d9c..63af99750be7c 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -5,16 +5,21 @@ @mouseout="onMouseOut" ref="codeNodeEditorContainer" > -
- - {{ $locale.baseText('codeNodeEditor.askAi') }} - + +
+ + + + + + +
@@ -22,7 +27,6 @@ import { defineComponent } from 'vue'; import type { PropType } from 'vue'; import { mapStores } from 'pinia'; - import type { LanguageSupport } from '@codemirror/language'; import type { Extension } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state'; @@ -44,10 +48,14 @@ import { CODE_PLACEHOLDERS } from './constants'; import { linterExtension } from './linter'; import { completerExtension } from './completer'; import { codeNodeEditorTheme } from './theme'; +import AskAI from './AskAI.vue'; export default defineComponent({ name: 'code-node-editor', mixins: [linterExtension, completerExtension, workflowHelpers], + components: { + AskAI, + }, props: { aiButtonEnabled: { type: Boolean, @@ -77,6 +85,9 @@ export default defineComponent({ linterCompartment: new Compartment(), isEditorHovered: false, isEditorFocused: false, + tabs: ['code', 'ask-ai'], + activeTab: 'code', + initialValue: this.value, }; }, watch: { @@ -105,6 +116,10 @@ export default defineComponent({ return this.editor.state.doc.toString(); }, + askAiEnabled(): boolean { + // TODO: Do this based on ENV? + return true; + }, placeholder(): string { return CODE_PLACEHOLDERS[this.language]?.[this.mode] ?? ''; }, @@ -120,6 +135,32 @@ export default defineComponent({ }, }, methods: { + async onReplaceCode(code: string) { + const hasChanges = this.initialValue !== this.content; + + if (hasChanges) { + const confirmModal = await this.alert( + this.$locale.baseText('codeNodeEditor.askAi.areYouSureToReplace'), + { + title: this.$locale.baseText('codeNodeEditor.askAi.replaceCurrentCode'), + confirmButtonText: this.$locale.baseText('codeNodeEditor.askAi.generateCodeAndReplace'), + showClose: true, + }, + ); + + if (confirmModal === 'cancel') { + this.activeTab = 'code'; + return; + } + } + + this.editor?.dispatch({ + changes: { from: 0, to: this.content.length, insert: code }, + }); + + this.initialValue = this.content; + this.activeTab = 'code'; + }, onMouseOver(event: MouseEvent) { const fromElement = event.relatedTarget as HTMLElement; const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; @@ -264,6 +305,29 @@ export default defineComponent({ diff --git a/packages/editor-ui/src/components/AiImportParameter.vue b/packages/editor-ui/src/components/AiImportParameter.vue new file mode 100644 index 0000000000000..af375e8627e0c --- /dev/null +++ b/packages/editor-ui/src/components/AiImportParameter.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/editor-ui/src/components/ImportCurlModal.vue b/packages/editor-ui/src/components/ImportCurlModal.vue index 7384559dffad0..26e7ab5747168 100644 --- a/packages/editor-ui/src/components/ImportCurlModal.vue +++ b/packages/editor-ui/src/components/ImportCurlModal.vue @@ -10,11 +10,10 @@
@@ -29,7 +28,7 @@ />
@@ -39,149 +38,39 @@ - diff --git a/packages/editor-ui/src/components/ImportParameter.vue b/packages/editor-ui/src/components/ImportParameter.vue index 3f9448765fb63..d51880f76a67a 100644 --- a/packages/editor-ui/src/components/ImportParameter.vue +++ b/packages/editor-ui/src/components/ImportParameter.vue @@ -1,13 +1,11 @@ - - diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 3746992e6bf1e..4dc61750807a7 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -91,6 +91,10 @@ + + + + - diff --git a/packages/editor-ui/src/components/ImportParameter.vue b/packages/editor-ui/src/components/ImportParameter.vue index d51880f76a67a..3f9448765fb63 100644 --- a/packages/editor-ui/src/components/ImportParameter.vue +++ b/packages/editor-ui/src/components/ImportParameter.vue @@ -1,11 +1,13 @@ + + diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 4dc61750807a7..3746992e6bf1e 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -91,10 +91,6 @@ - - - -