diff --git a/FeatureFlags.js b/FeatureFlags.js index 87bedb90b..f2f37756c 100644 --- a/FeatureFlags.js +++ b/FeatureFlags.js @@ -45,6 +45,9 @@ module.exports = { // Whether the Chatbot UserInterface and its functionality should be enabled enableChatbot: false, + // AI Chatbot in the BPMN modeler view. + enableBPMNChatbot: true, + //feature to switch to prisma from fs enableUseDB: true, //feature to use GCP_bucket / fs depending on deployment env to store blobs diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx index 7fb330ff3..b59a54e82 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx @@ -11,6 +11,7 @@ import Icon, { ArrowUpOutlined, FormOutlined, ShareAltOutlined, + RobotOutlined, } from '@ant-design/icons'; import { GrDocumentUser } from 'react-icons/gr'; import { PiDownloadSimple } from 'react-icons/pi'; @@ -41,6 +42,8 @@ import { useCanEdit } from '@/lib/can-edit-context'; import { Element } from 'bpmn-js/lib/model/Types'; import { ScriptTaskEditorEnvironment } from './script-task-editor/script-task-editor-environment'; import { Folder } from '@/lib/data/folder-schema'; +import { enableBPMNChatbot } from 'FeatureFlags'; +import ChatbotDialog from '@/components/bpmn-chatbot'; const LATEST_VERSION = { id: '-1', name: 'Latest Version', description: '' }; @@ -261,6 +264,7 @@ const ModelerToolbar = ({ LATEST_VERSION; const showMobileView = useMobileModeler(); + const [showChatbotDialog, setShowChatbotDialog] = useState(false); let formEditorTitle = ''; @@ -440,9 +444,16 @@ const ModelerToolbar = ({ }} /> + )}{' '} + {enableBPMNChatbot && ( + + + )} - {showPropertiesPanel && selectedElement && ( )} + {enableBPMNChatbot && ( + + )} diff --git a/src/management-system-v2/components/bpmn-chatbot.tsx b/src/management-system-v2/components/bpmn-chatbot.tsx new file mode 100644 index 000000000..be997f9a8 --- /dev/null +++ b/src/management-system-v2/components/bpmn-chatbot.tsx @@ -0,0 +1,289 @@ +import { BPMNCanvasRef } from '@/components/bpmn-canvas'; +import { Button, Card, Form, Input, List, Space, Tag, Tooltip } from 'antd'; +import React, { ReactNode, useRef, useState } from 'react'; +import { getNewShapePosition } from 'bpmn-js/lib/features/auto-place/BpmnAutoPlaceUtil'; +import { Element, Shape } from 'bpmn-js/lib/model/Types'; +import { FileOutlined, MessageOutlined, SendOutlined } from '@ant-design/icons'; +import { FaIcons } from 'react-icons/fa6'; +import { set } from 'zod'; + +type ChatbotDialogProps = { + show: boolean; + modeler: BPMNCanvasRef | null; +}; + +type FieldType = { + prompt: string; +}; + +const ChatbotDialog: React.FC = ({ show, modeler }) => { + const [lastPrompts, setLastPrompts] = useState< + { message: ReactNode; isUser: boolean; bpmn?: string }[] + >([]); + const [waitForResponse, setWaitForResponse] = useState(false); + const [prompt, setPrompt] = useState(''); + const root = modeler!.getCurrentRoot(); + const modeling = modeler!.getModeling(); + //const elementFactory = modeler!.getElementFactory(); + + function onPrompt({ prompt }: FieldType) { + setWaitForResponse(true); + getProcessXml().then((process) => { + /*sendToAPI(prompt, process) + .then((res) => { + if (res) { + processResponse(res); + } + setLastPrompts( + lastPrompts.concat({ + userPrompt: prompt, + bpmnProcess: process, + chatbotResponse: res, + }), + ); + }) + .finally(() => setWaitForResponse(false));*/ + }); + } + + //see tools definitions + function append_shape( + bpmn_type: string, + new_element_name: string, + source_element_id_or_name: string, + created: { name: string; shape: Shape }[], + label: string, + ) { + let source = modeler?.getElement(source_element_id_or_name) as Shape; + if (!source) { + console.log(created); + source = created.find((e) => e.name == source_element_id_or_name)!.shape; + } + /*let shape = elementFactory.createShape({ type: 'bpmn:' + bpmn_type }); + const position = getNewShapePosition(source, shape); + shape = modeling.createShape({ type: 'bpmn:' + bpmn_type }, position, root!); + const connection = modeling.createConnection( + source, + shape, + { type: 'bpmn:SequenceFlow' }, + root!, + ); + modeling.updateLabel(connection, label); + modeling.updateLabel(shape, new_element_name); + return shape;*/ + } + function create_connection( + source_element_id_or_name: string, + target_element_id_or_name: string, + created: { name: string; shape: Shape }[], + label?: string, + ): void { + let source = modeler?.getElement(source_element_id_or_name) as Shape; + if (!source) { + source = created.find((e) => e.name == source_element_id_or_name)!.shape; + } + let target = modeler?.getElement(target_element_id_or_name) as Shape; + if (!target) { + target = created.find((e) => e.name == target_element_id_or_name)!.shape; + } + const connection = modeling.createConnection( + source, + target, + { type: 'bpmn:SequenceFlow' }, + root!, + ); + if (label) { + modeling.updateLabel(connection, label); + } + } + function remove_elements(element_ids: string[]): void { + const elements: Element[] = []; + element_ids.forEach((e) => { + const element = modeler?.getElement(e); + if (element) { + elements.push(element); + } + }); + + modeling.removeElements(elements); + } + + //parsing tool uses listed in response + function processResponse(response: any[]) { + const created: { name: string; shape: Shape }[] = []; + response.forEach((res) => { + if (res.name == 'create_connection') { + create_connection( + res.args.source_element_id_or_name, + res.args.target_element_id_or_name, + created, + res.args.label, + ); + } else if (res.name == 'remove_elements') { + remove_elements(res.args.element_ids); + } else if (res.name == 'append_element') { + const shape = append_shape( + res.args.bpmn_type, + res.args.name, + res.args.source_element_id_or_name, + created, + res.args.label, + ); + created.push({ name: res.args.name, shape: shape as any }); + } + }); + } + + const onSubmit = async () => { + setPrompt(''); + setLastPrompts((lastPrompts) => lastPrompts.concat({ message: prompt, isUser: true })); + scrollToBottom(); + await new Promise((resolve) => setTimeout(resolve, 2000)); + if (lastPrompts.filter((p) => !p.isUser).length == 0) { + setLastPrompts((lastPrompts) => + lastPrompts.concat({ + message: "Here's a simple BPMN representation for a vacation application process:", + isUser: false, + bpmn: "", + }), + ); + } else if (lastPrompts.filter((p) => !p.isUser).length == 1) { + setLastPrompts((lastPrompts) => + lastPrompts.concat({ + message: + "Sure, here's an updated BPMN process that includes a form reference for the vacation request task:", + isUser: false, + bpmn: "", + }), + ); + } else if (lastPrompts.filter((p) => !p.isUser).length == 2) { + setLastPrompts((lastPrompts) => + lastPrompts.concat({ + message: ( +
+

Here's the updated BPMN XML with element colors.

+

Changes and Features Added:

+
    +
  1. + Form Attachment: +
      +
    • + The VacationApplicationTask has a form with fields like employeeName, + vacationStart, vacationEnd, and reason. +
    • +
    +
  2. +
  3. + Color Customization +
      +
    • Green (#5cb85c) for Start Event and Approved End Event.
    • +
    • Blue (#337ab7) for the Vacation Request Task.
    • +
    • Orange (#f0ad4e) for Manager Approval.
    • +
    • Red (#d9534f) for Rejection Gateway and Notification.
    • +
    • Light Blue (#5bc0de) for HR Processing.
    • +
    +
  4. +
+
+ ), + isUser: false, + bpmn: "", + }), + ); + } else { + setLastPrompts((lastPrompts) => + lastPrompts.concat({ + message: 'I am a simple chatbot and cannot do much more than this.', + isUser: false, + }), + ); + } + scrollToBottom(); + }; + + //get current xml of the ... part only + function getProcessXml(): Promise { + return modeler!.getXML().then((res) => { + if (res) { + const startIndex = res.indexOf('', startIndex); + if (endIndex == -1) { + return ''; + } + return res.slice(startIndex, endIndex) + ''; + } else { + return ''; + } + }); + } + + const listRef = useRef(null); + + const scrollToBottom = () => { + if (listRef.current) { + setTimeout(() => { + listRef.current!.scrollTop = listRef.current!.scrollHeight; + }, 100); + } + }; + + return ( + <> + + + ); +}; +export default ChatbotDialog;