From cac93e12204c7ae228bbc57bfb8735be85f32f13 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 6 Jul 2023 09:49:19 +0200 Subject: [PATCH] feat(copilot): scaffold copilot --- .env | 1 + assets/bpmn-js.css | 4 + lib/Modeler.js | 2 + lib/features/copilot/Copilot.js | 138 +++++++++++++++++++++++++ lib/features/copilot/OpenAIProvider.js | 43 ++++++++ lib/features/copilot/index.js | 15 +++ package-lock.json | 58 +++++++++++ package.json | 1 + test/config/karma.unit.js | 6 +- test/spec/ModelerSpec.js | 2 +- 10 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 .env create mode 100644 lib/features/copilot/Copilot.js create mode 100644 lib/features/copilot/OpenAIProvider.js create mode 100644 lib/features/copilot/index.js diff --git a/.env b/.env new file mode 100644 index 0000000000..bc9d1b63a3 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +OPENAI_API_KEY="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" \ No newline at end of file diff --git a/assets/bpmn-js.css b/assets/bpmn-js.css index 84f98db409..ea52431cf4 100644 --- a/assets/bpmn-js.css +++ b/assets/bpmn-js.css @@ -142,3 +142,7 @@ height: 20px; width: 20px; } + +.layer-copilot { + opacity: 0.5; +} diff --git a/lib/Modeler.js b/lib/Modeler.js index 3c50af1b2a..3749e9c7f3 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -18,6 +18,7 @@ import BendpointsModule from 'diagram-js/lib/features/bendpoints'; import ConnectModule from 'diagram-js/lib/features/connect'; import ConnectionPreviewModule from 'diagram-js/lib/features/connection-preview'; import ContextPadModule from './features/context-pad'; +import CopilotModule from './features/copilot'; import CopyPasteModule from './features/copy-paste'; import CreateModule from 'diagram-js/lib/features/create'; import DistributeElementsModule from './features/distribute-elements'; @@ -175,6 +176,7 @@ Modeler.prototype._modelingModules = [ ConnectModule, ConnectionPreviewModule, ContextPadModule, + CopilotModule, CopyPasteModule, CreateModule, DistributeElementsModule, diff --git a/lib/features/copilot/Copilot.js b/lib/features/copilot/Copilot.js new file mode 100644 index 0000000000..e1dc507d84 --- /dev/null +++ b/lib/features/copilot/Copilot.js @@ -0,0 +1,138 @@ +import { clear as svgClear } from 'tiny-svg'; + +export default class Copilot { + constructor( + canvas, + createPreview, + directEditing, + eventBus, + keyboard, + modeling, + selection) { + this._canvas = canvas; + this._createPreview = createPreview; + this._directEditing = directEditing; + this._modeling = modeling; + this._selection = selection; + + eventBus.on('selection.changed', (context) => { + const { newSelection } = context; + + this.clearSuggestion(); + + if (newSelection.length === 1) { + this.makeSuggestion(newSelection[ 0 ]); + } + }); + + eventBus.on('directEditing.activate', () => { + this.hideSuggestion(); + }); + + eventBus.on('directEditing.deactivate', () => { + this.showSuggestion(); + }); + + keyboard.addListener((context) => { + if (context.keyEvent.key === 'Tab') { + context.keyEvent.preventDefault(); + + this.acceptSuggestion(); + } else if (context.keyEvent.key === 'Escape') { + this.clearSuggestion(); + } + }); + + this._suggestion = null; + + this._layer = canvas.getLayer('copilot', 1000); + } + + acceptSuggestion() { + console.log('acceptSuggestion'); + + if (!this._suggestion) { + return; + } + + const { + elements, + position, + target + } = this._suggestion; + + const createdElements = this._modeling.createElements(elements, position, target, { + position: 'absolute' + }); + + this.clearSuggestion(); + + if (createdElements.length === 1) { + this._selection.select(createdElements[ 0 ]); + + this._directEditing.activate(createdElements[ 0 ]); + } + } + + makeSuggestion(element) { + console.log('makeSuggestion'); + + const suggestion = this._suggestion = this.createSuggestion(element); + + console.log('suggestion', suggestion); + + if (!suggestion) { + return; + } + + const { elements } = suggestion; + + const group = this._createPreview.createPreview(elements); + + this._layer.appendChild(group); + } + + showSuggestion() { + console.log('showSuggestion'); + + this._canvas.showLayer('copilot'); + } + + hideSuggestion() { + console.log('hideSuggestion'); + + this._canvas.hideLayer('copilot'); + } + + clearSuggestion() { + console.log('clearSuggestion'); + + this._suggestion = null; + + svgClear(this._layer); + } + + createSuggestion(element) { + console.log('createSuggestion'); + + if (!this._provider) { + return null; + } + + return this._provider.createSuggestion(element); + } + + setProvider(provider) { + this._provider = provider; + } +} + +Copilot.$inject = [ + 'canvas', + 'createPreview', + 'directEditing', + 'eventBus', + 'keyboard', + 'modeling', + 'selection' +]; diff --git a/lib/features/copilot/OpenAIProvider.js b/lib/features/copilot/OpenAIProvider.js new file mode 100644 index 0000000000..6c7c16125c --- /dev/null +++ b/lib/features/copilot/OpenAIProvider.js @@ -0,0 +1,43 @@ +import { is } from '../../util/ModelUtil'; + +import { getMid } from 'diagram-js/lib/layout/LayoutUtil'; + +const apiKey = process.env.OPENAI_API_KEY; + +export default class OpenAIProvider { + constructor(copilot, elementFactory) { + copilot.setProvider(this); + + this._elementFactory = elementFactory; + } + + createSuggestion(element) { + const suggestion = { + elements: [], + position: null, + target: element.parent + }; + + const mid = getMid(element); + + if (is(element, 'bpmn:Task')) { + return { + ...suggestion, + elements: [ + this._elementFactory.createShape({ + type: 'bpmn:Task', + x: element.x + element.width + 100, + y: element.y + }) + ] + }; + } + + return null; + } +} + +OpenAIProvider.$inject = [ + 'copilot', + 'elementFactory' +]; \ No newline at end of file diff --git a/lib/features/copilot/index.js b/lib/features/copilot/index.js new file mode 100644 index 0000000000..9b57b2a00d --- /dev/null +++ b/lib/features/copilot/index.js @@ -0,0 +1,15 @@ +import Copilot from './Copilot'; +import OpenAIProvider from './OpenAIProvider'; + +export default { + __depends__: [ + + // TODO + ], + __init__: [ + 'copilot', + 'openaiprovider' + ], + copilot: [ 'type', Copilot ], + openaiprovider: [ 'type', OpenAIProvider ] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a1cf9a9a3e..942e9ea1e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "cross-env": "^7.0.3", "del": "^6.0.0", "del-cli": "^5.0.0", + "dotenv-webpack": "^8.0.1", "eslint": "^8.22.0", "eslint-plugin-bpmn-io": "^1.0.0", "eslint-plugin-import": "^2.26.0", @@ -3931,6 +3932,39 @@ "resolved": "https://registry.npmjs.org/domify/-/domify-1.4.1.tgz", "integrity": "sha512-x18nuiDHMCZGXr4KJSRMf/TWYtiaRo6RX8KN9fEbW54mvbQ6pieUuerC2ahBg+kEp1wycFj8MPUI0WkIOw5E9w==" }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", + "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "dev": true, + "dependencies": { + "dotenv": "^8.2.0" + } + }, + "node_modules/dotenv-webpack": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz", + "integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==", + "dev": true, + "dependencies": { + "dotenv-defaults": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "webpack": "^4 || ^5" + } + }, "node_modules/duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -20331,6 +20365,30 @@ "resolved": "https://registry.npmjs.org/domify/-/domify-1.4.1.tgz", "integrity": "sha512-x18nuiDHMCZGXr4KJSRMf/TWYtiaRo6RX8KN9fEbW54mvbQ6pieUuerC2ahBg+kEp1wycFj8MPUI0WkIOw5E9w==" }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true + }, + "dotenv-defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", + "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "dev": true, + "requires": { + "dotenv": "^8.2.0" + } + }, + "dotenv-webpack": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz", + "integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==", + "dev": true, + "requires": { + "dotenv-defaults": "^2.0.2" + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", diff --git a/package.json b/package.json index 04d4c50eb2..32f36d1472 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "cross-env": "^7.0.3", "del": "^6.0.0", "del-cli": "^5.0.0", + "dotenv-webpack": "^8.0.1", "eslint": "^8.22.0", "eslint-plugin-bpmn-io": "^1.0.0", "eslint-plugin-import": "^2.26.0", diff --git a/test/config/karma.unit.js b/test/config/karma.unit.js index 3956203039..ad50a66504 100644 --- a/test/config/karma.unit.js +++ b/test/config/karma.unit.js @@ -19,6 +19,7 @@ var absoluteBasePath = path.resolve(path.join(__dirname, basePath)); var suite = coverage ? 'test/coverageBundle.js' : 'test/testBundle.js'; +const Dotenv = require('dotenv-webpack'); module.exports = function(karma) { @@ -101,7 +102,10 @@ module.exports = function(karma) { absoluteBasePath ] }, - devtool: 'eval-source-map' + devtool: 'eval-source-map', + plugins: [ + new Dotenv() + ] } }; diff --git a/test/spec/ModelerSpec.js b/test/spec/ModelerSpec.js index c128200fbe..c223c9c5c7 100644 --- a/test/spec/ModelerSpec.js +++ b/test/spec/ModelerSpec.js @@ -53,7 +53,7 @@ describe('Modeler', function() { setBpmnJS(modeler); - enableLogging(modeler, singleStart); + // enableLogging(modeler, singleStart); return modeler.importXML(xml).then(function(result) { return { error: null, warnings: result.warnings, modeler: modeler };