From deda6ec40c1caf0daf05b36779f65be0b430bc02 Mon Sep 17 00:00:00 2001 From: Riley Evans Date: Mon, 2 Mar 2026 14:12:24 -0600 Subject: [PATCH 01/15] Added basic workflow modification support --- Localize/lang/strings.json | 20 +- .../app/AzureLogicAppsDesigner/laDesigner.tsx | 27 +- .../AzureLogicAppsDesigner/laDesignerV2.tsx | 27 +- libs/chatbot/src/lib/ui/ChatbotUi.tsx | 29 +- libs/chatbot/src/lib/ui/CopilotChatbot.tsx | 275 +++++++++++++++--- libs/designer-ui/src/lib/chatbot/chatbot.less | 20 +- .../lib/chatbot/components/assistantError.tsx | 4 +- .../chatbot/components/assistantGreeting.tsx | 69 +++-- .../components/assistantReplyWithFlow.tsx | 6 +- .../src/lib/chatbot/components/chatBubble.tsx | 80 ++--- .../lib/chatbot/components/chatInputBox.tsx | 257 ++++++++-------- .../components/connectionsSetupMessage.tsx | 7 +- .../chatbot/components/conversationItem.ts | 2 + .../components/thumbsReactionButton.tsx | 60 ++-- .../src/lib/chatbot/feedbackHelper.tsx | 30 +- libs/designer-ui/src/lib/chatbot/index.tsx | 2 + .../createNaturalLanguageToFlowInput.tsx | 34 +-- .../src/designer-client-services/index.ts | 1 + .../lib/base/copilotWorkflowEditor.ts | 231 +++++++++++++++ .../lib/base/index.ts | 3 + .../lib/copilotWorkflowEditor.ts | 39 +++ 21 files changed, 828 insertions(+), 395 deletions(-) create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/base/copilotWorkflowEditor.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/copilotWorkflowEditor.ts diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index cb52fb1dc0a..c3ad17a8a03 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -140,11 +140,13 @@ "1D047X": "Required. The value to convert to XML.", "1Fn5n+": "Required. The URI encoded string.", "1GWzEL": "No results found for the specified filters", + "1Gsrs4": "You can ask questions or describe changes you want to make to your workflow.", "1HhCtq": "Headers", "1KFpTX": "(UTC+03:00) Minsk", "1KMc+6": "The integer should be between [{min}, {max}]", "1LSKq8": "Basics", "1NBvKu": "Convert the parameter argument to a floating-point number", + "1NVeRR": "This assistant can help you learn about your workflows, answer questions about Azure Logic Apps, and make changes to your workflow.", "1Orv4i": "Details", "1REu5/": "See less", "1Xke9D": "open functions drawer", @@ -866,6 +868,7 @@ "I2Ztna": "Loop automatically added when connecting a repeating source element. No function required.", "I3mifR": "Is skipped", "I41vZ/": "(UTC-11:00) Coordinated Universal Time-11", + "I9lfbc": "✅ Workflow updated successfully.", "IA+Ogm": "22", "IAmvpa": "(UTC-08:00) Coordinated Universal Time-08", "IBFBR2": "Remove loop", @@ -1137,6 +1140,7 @@ "OdNhwc": "Ungroup", "OeSQhS": "Create a new Azure Storage Account", "Oep6va": "Submit", + "OevhEs": "↩️ Workflow change has been undone.", "OgJ9eG": "(UTC+08:00) Taipei", "OhbvXz": "(UTC+11:00) Norfolk Island", "Oib1mL": "{hours}h {minutes}m", @@ -1280,6 +1284,7 @@ "RvpHdu": "(UTC+11:00) Solomon Is., New Caledonia", "RxGxr+": "Line number", "RxbkcI": "Unsupported token type: {controls}", + "S+9l11": "Ask a question or describe a workflow change...", "S0N/tx": "Resubmit a workflow run from this action", "S138/4": "Format text as bold. Shortcut: ⌘B", "S2KtbJ": "Select date and time", @@ -1460,6 +1465,7 @@ "Vq9q5J": "Built-in", "Vqs8hE": "Actions", "VtRDnx": "Set the valid duration for this API key. Securely save the key after generation. This key appears only once and isn't stored in Azure, so you can't view the key later. Lost API keys require regenerating new ones.", + "VuvZff": "Add a response action that returns a 200 status code.", "Vx6fwP": "Added this action", "VysSj3": "View code", "W+mUyI": "Next", @@ -1794,11 +1800,13 @@ "_1D047X.comment": "Required string parameter to be converted using xml function", "_1Fn5n+.comment": "Required URI encoded string parameter to be converted using uriComponentToBinary function", "_1GWzEL.comment": "Text displayed when no results are found in the browse grid", + "_1Gsrs4.comment": "Chatbot outro message when workflow editing is enabled", "_1HhCtq.comment": "headers", "_1KFpTX.comment": "Time zone value ", "_1KMc+6.comment": "Error validation message for integers being out of range", "_1LSKq8.comment": "Accessibility label for the basics section", "_1NBvKu.comment": "Label for description of custom float Function", + "_1NVeRR.comment": "Chatbot introduction message when workflow editing is enabled", "_1Orv4i.comment": "Title for the details section", "_1REu5/.comment": "Select to view fewer token options.", "_1Xke9D.comment": "aria label to open functions drawer", @@ -2520,6 +2528,7 @@ "_I2Ztna.comment": "Message explaining user does not need to add a loop function", "_I3mifR.comment": "Skipped run", "_I41vZ/.comment": "Time zone value ", + "_I9lfbc.comment": "Chatbot message confirming a workflow modification was applied", "_IA+Ogm.comment": "Hour of the day", "_IAmvpa.comment": "Time zone value ", "_IBFBR2.comment": "Remove loop for the connection", @@ -2791,6 +2800,7 @@ "_OdNhwc.comment": "Ungroup button", "_OeSQhS.comment": "Description for the Azure Storage Account create popup", "_Oep6va.comment": "Submit button", + "_OevhEs.comment": "Chatbot message confirming a workflow modification was undone", "_OgJ9eG.comment": "Time zone value ", "_OhbvXz.comment": "Time zone value ", "_Oib1mL.comment": "This is a time duration in abbreviated format", @@ -2934,6 +2944,7 @@ "_RvpHdu.comment": "Time zone value ", "_RxGxr+.comment": "The title of the line number field in the static result parseJson action", "_RxbkcI.comment": "Exception for unsupported token types", + "_S+9l11.comment": "Chatbot input placeholder when workflow editing is enabled", "_S0N/tx.comment": "accessibility text for the resubmit button", "_S138/4.comment": "label to make bold text for Mac users", "_S2KtbJ.comment": "Label for custom date time picker", @@ -3114,6 +3125,7 @@ "_Vq9q5J.comment": "Filter by In App category of connectors", "_Vqs8hE.comment": "Actions button", "_VtRDnx.comment": "Description for the MCP generate keys section", + "_VuvZff.comment": "Chatbot suggestion message to add a response action to the workflow", "_Vx6fwP.comment": "Chatbot added operation sentence format", "_VysSj3.comment": "Button for View Code", "_W+mUyI.comment": "Placeholder text for the Next button in the suggested workflow description", @@ -3619,6 +3631,7 @@ "_h8JTcA.comment": "Shows how many tools are selected", "_hA5Aif.comment": "The tab label for the publish tab on the configure template wizard", "_hCrg+6.comment": "Cannot delete the last run after edge", + "_hFjNA8.comment": "Chatbot introduction message to suggest what it can help with", "_hGbRBS.comment": "All tab label", "_hHDJhD.comment": "Placeholder title for a newly inserted File parameter", "_hHNj31.comment": "Cancel button text", @@ -3758,7 +3771,6 @@ "_kAJqcb.comment": "Description for parameters step", "_kBSLfu.comment": "Duplicate property name error message", "_kEI2xx.comment": "The title of the message field in the static result parseJson action", - "_kEjmTx.comment": "Chatbot introduction message to suggest what it can help with", "_kH7x1w.comment": "Label for the App Service plan field", "_kHcCxH.comment": "This is a time duration in abbreviated format", "_kHs5R4.comment": "Chatbot flow preview message reminding user to check workflow actions", @@ -3812,6 +3824,7 @@ "_lL5oRE.comment": "Microsoft tab label", "_lLhS3T.comment": "Button text for creating new workflows", "_lM9qrG.comment": "Time zone value ", + "_lO+med.comment": "Chatbot message when a workflow modification is proposed for review", "_lPTdSf.comment": "Button text for run trigger", "_lPj8hf.comment": "Description for create connection step", "_lQNKUB.comment": "Describes connection being added", @@ -3994,6 +4007,7 @@ "_p0BE2D.comment": "Button text to trigger clone in the create workflow panel", "_p1IEXb.comment": "Label for button to open dynamic content token picker", "_p2eSD1.comment": "Button text for opening panel for editing workflows", + "_p4+r7z.comment": "Chatbot suggestion message to add error handling to the workflow", "_p5ZID0.comment": "Time zone value ", "_p8AKOz.comment": "Label for the description textfield", "_pC2nr2.comment": "Placeholder text for Key", @@ -4805,6 +4819,7 @@ "h8JTcA": "{count} {count, plural, one {tool} other {tools}} selected", "hA5Aif": "Publish", "hCrg+6": "Actions must have one or more run after configurations", + "hFjNA8": "Some things you can try:", "hGbRBS": "All", "hHDJhD": "File content", "hHNj31": "Cancel", @@ -4944,7 +4959,6 @@ "kAJqcb": "Configure the parameters for this MCP tool", "kBSLfu": "Duplicate property name", "kEI2xx": "Message", - "kEjmTx": "Some things you can ask:", "kH7x1w": "App Service plan", "kHcCxH": "{minutes}m {seconds}s", "kHs5R4": "Check these actions to see if any parameters need to be set.", @@ -4998,6 +5012,7 @@ "lL5oRE": "Microsoft", "lLhS3T": "Create new workflows", "lM9qrG": "(UTC+13:00) Nuku'alofa", + "lO+med": "I have a workflow change ready. Review the proposal below.", "lPTdSf": "Run trigger", "lPj8hf": "Create a new connection for the MCP server.", "lQNKUB": "A line for the parent element is added automatically.", @@ -5180,6 +5195,7 @@ "p0BE2D": "Clone", "p1IEXb": "Enter the data from previous step. You can also add data by typing the '/' character.", "p2eSD1": "Edit", + "p4+r7z": "Add error handling to this workflow.", "p5ZID0": "(UTC+03:00) Kuwait, Riyadh", "p8AKOz": "Description", "pC2nr2": "Enter key", diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx index 82eb3bd382f..e97784cc775 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx @@ -32,6 +32,8 @@ import { BaseApiManagementService, BaseAppServiceService, BaseChatbotService, + BaseCopilotWorkflowEditorService, + InitCopilotWorkflowEditorService, BaseExperimentationService, BaseUserPreferenceService, BaseFunctionService, @@ -86,7 +88,6 @@ const httpClient = new HttpClient(); const DesignerEditor = () => { const { id: workflowId } = useSelector((state: RootState) => ({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: state.workflowLoader.resourcePath!, })); @@ -510,6 +511,14 @@ const DesignerEditor = () => { getUpdatedWorkflow={getUpdatedWorkflow} openFeedbackPanel={() => openPanel('Azure Feedback Panel has been opened')} closeChatBot={() => dispatch(setIsChatBotEnabled(false))} + enableWorkflowEditing={true} + autoApply={true} + onWorkflowProposed={(newWorkflow) => { + setWorkflow({ + ...newWorkflow, + id: guid(), + }); + }} /> ) : null}
{ const { id: workflowId } = useSelector((state: RootState) => ({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: state.workflowLoader.resourcePath!, })); @@ -645,6 +646,14 @@ const DesignerEditor = () => { getUpdatedWorkflow={getUpdatedWorkflow} openFeedbackPanel={() => openPanel('Azure Feedback Panel has been opened')} closeChatBot={() => dispatch(setIsChatBotEnabled(false))} + enableWorkflowEditing={true} + autoApply={true} + onWorkflowProposed={(newWorkflow) => { + setWorkflow({ + ...newWorkflow, + id: guid(), + }); + }} /> ) : null}
{ } = props; const intl = useIntl(); const { isInverted } = useTheme(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); // Styles const styles = useChatbotStyles(); const darkStyles = useChatbotDarkStyles(); - const inputIconButtonStyles = useMemo( - () => ({ - enabled: { - root: { - backgroundColor: 'transparent', - color: isInverted ? 'rgb(200, 200, 200)' : 'rgb(51, 51, 51)', - }, - }, - disabled: { - root: { - backgroundColor: 'transparent', - color: isInverted ? 'rgb(79, 79, 79)' : 'rgb(200, 200, 200)', - }, - }, - }), - [isInverted] - ); - useEffect(() => { if (focus) { textInputRef.current?.focus(); @@ -174,7 +157,7 @@ export const ChatbotUI = (props: ChatbotUIProps) => { ) : null} {readOnly ? null : ( { submitButtonProps={{ title: submitString ?? intlText.submitButton, disabled: submitDisabled, - iconProps: { - iconName: 'Send', - styles: submitDisabled ? inputIconButtonStyles.disabled : inputIconButtonStyles.enabled, - }, onClick: () => onSubmit(value), }} /> diff --git a/libs/chatbot/src/lib/ui/CopilotChatbot.tsx b/libs/chatbot/src/lib/ui/CopilotChatbot.tsx index 9e6f28905da..8da6009187d 100644 --- a/libs/chatbot/src/lib/ui/CopilotChatbot.tsx +++ b/libs/chatbot/src/lib/ui/CopilotChatbot.tsx @@ -3,14 +3,22 @@ import type { RequestData } from '../common/models/Query'; import { isSuccessResponse } from '../core/util'; import { CopilotPanelHeader } from './panelheader'; import { getId } from '@fluentui/react'; -import { LogEntryLevel, LoggerService, ChatbotService, guid, type Workflow } from '@microsoft/logic-apps-shared'; +import { + LogEntryLevel, + LoggerService, + ChatbotService, + CopilotWorkflowEditorService, + isCopilotWorkflowEditorServiceInitialized, + guid, + type Workflow, +} from '@microsoft/logic-apps-shared'; import type { ConversationItem, ChatEntryReaction, AdditionalParametersItem } from '@microsoft/designer-ui'; -import { PanelLocation, ConversationItemType, FlowOrigin } from '@microsoft/designer-ui'; +import { PanelLocation, ConversationItemType, FlowOrigin, UndoStatus } from '@microsoft/designer-ui'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import { AssistantChat, defaultChatbotPanelWidth } from './ChatbotUi'; -interface CoPilotChatbotProps { +export interface CoPilotChatbotProps { panelLocation?: PanelLocation; getAuthToken: () => Promise; getUpdatedWorkflow: () => Promise; @@ -18,6 +26,12 @@ interface CoPilotChatbotProps { openAzureCopilotPanel?: (prompt?: string) => void; // callback to open Azure Copilot Panel closeChatBot?: () => void; // callback when chatbot is closed chatbotWidth?: string; + /** When true and CopilotWorkflowEditorService is initialized, workflow edit requests go through the editor service */ + enableWorkflowEditing?: boolean; + /** Called when a workflow modification is proposed/approved. The host is responsible for applying the update. */ + onWorkflowProposed?: (workflow: Workflow) => void; + /** When true, workflow proposals are applied immediately without showing a proposal card. Defaults to true. */ + autoApply?: boolean; } export const CoPilotChatbot = ({ @@ -28,10 +42,14 @@ export const CoPilotChatbot = ({ openAzureCopilotPanel, closeChatBot, chatbotWidth = defaultChatbotPanelWidth, + enableWorkflowEditing = false, + onWorkflowProposed, + autoApply = true, }: CoPilotChatbotProps) => { const chatSessionId = useRef(guid()); const intl = useIntl(); const chatbotService = ChatbotService(); + const workflowEditorEnabled = enableWorkflowEditing && isCopilotWorkflowEditorServiceInitialized(); const [inputQuery, setInputQuery] = useState(''); const [collapsed, setCollapsed] = useState(false); const [answerGeneration, stopAnswerGeneration] = useState(true); @@ -46,19 +64,28 @@ export const CoPilotChatbot = ({ id: getId(), date: new Date(), reaction: undefined, + workflowEditingEnabled: workflowEditorEnabled, }, ]); const [controller, setController] = useState(new AbortController()); const signal = controller.signal; const [selectedOperation] = useState(''); + // Track the last proposed workflow for undo support + const pendingProposalRef = useRef<{ workflow: Workflow; previousWorkflow: Workflow; conversationItemId: string } | null>(null); const intlText = useMemo(() => { return { - chatInputPlaceholder: intl.formatMessage({ - defaultMessage: 'Ask a question about this workflow or about Azure Logic Apps as a whole ...', - id: 'kXn5e0', - description: 'Chabot input placeholder text', - }), + chatInputPlaceholder: workflowEditorEnabled + ? intl.formatMessage({ + defaultMessage: 'Ask a question or describe a workflow change...', + id: 'S+9l11', + description: 'Chatbot input placeholder when workflow editing is enabled', + }) + : intl.formatMessage({ + defaultMessage: 'Ask a question about this workflow or about Azure Logic Apps as a whole ...', + id: 'kXn5e0', + description: 'Chabot input placeholder text', + }), protectedMessage: intl.formatMessage({ defaultMessage: 'Your personal and company data are protected in this chat', id: 'Yrw/Qt', @@ -165,8 +192,23 @@ export const CoPilotChatbot = ({ id: 'JKZpcd', description: 'Chatbot card telling user that the AI response is being canceled', }), + workflowAppliedText: intl.formatMessage({ + defaultMessage: '✅ Workflow updated successfully.', + id: 'I9lfbc', + description: 'Chatbot message confirming a workflow modification was applied', + }), + workflowProposedText: intl.formatMessage({ + defaultMessage: 'I have a workflow change ready. Review the proposal below.', + id: 'lO+med', + description: 'Chatbot message when a workflow modification is proposed for review', + }), + workflowUndoneText: intl.formatMessage({ + defaultMessage: '↩️ Workflow change has been undone.', + id: 'OevhEs', + description: 'Chatbot message confirming a workflow modification was undone', + }), }; - }, [intl, selectedOperation]); + }, [intl, selectedOperation, workflowEditorEnabled]); const logFeedbackVote = useCallback((reaction: ChatEntryReaction, isRemovedVote?: boolean) => { if (isRemovedVote) { @@ -184,6 +226,182 @@ export const CoPilotChatbot = ({ } }, []); + const handleWorkflowEditResponse = useCallback( + async (query: string, requestPayload: RequestData) => { + const editorService = CopilotWorkflowEditorService(); + const currentWorkflow = await getUpdatedWorkflow(); + const response = await editorService.getWorkflowEdit(query, currentWorkflow, signal); + + if (response.type === 'workflow' && response.workflow) { + const proposedWorkflow = response.workflow; + const responseId = guid(); + + if (autoApply) { + // Auto-apply: immediately call onWorkflowProposed and show confirmation + pendingProposalRef.current = { + workflow: proposedWorkflow, + previousWorkflow: currentWorkflow, + conversationItemId: responseId, + }; + onWorkflowProposed?.(proposedWorkflow); + + setConversation((current) => [ + { + type: ConversationItemType.ReplyWithFlow, + id: responseId, + date: new Date(), + text: response.text || intlText.workflowAppliedText, + reaction: undefined, + undoStatus: UndoStatus.UndoAvailable, + correlationId: chatSessionId.current, + __rawRequest: requestPayload, + __rawResponse: response, + openFeedback: openFeedbackPanel, + logFeedbackVote, + onClick: () => { + // Undo handler: revert to previous workflow + const proposal = pendingProposalRef.current; + if (proposal && proposal.conversationItemId === responseId) { + onWorkflowProposed?.(proposal.previousWorkflow); + pendingProposalRef.current = null; + setConversation((current) => + current.map((item) => + item.id === responseId + ? { + ...item, + undoStatus: UndoStatus.Undone, + text: intlText.workflowUndoneText, + } + : item + ) + ); + } + }, + }, + ...current, + ]); + } else { + // Proposal mode: show the proposal for user approval + pendingProposalRef.current = { + workflow: proposedWorkflow, + previousWorkflow: currentWorkflow, + conversationItemId: responseId, + }; + + setConversation((current) => [ + { + type: ConversationItemType.ReplyWithFlow, + id: responseId, + date: new Date(), + text: response.text || intlText.workflowProposedText, + reaction: undefined, + undoStatus: UndoStatus.Unavailable, + correlationId: chatSessionId.current, + __rawRequest: requestPayload, + __rawResponse: response, + openFeedback: openFeedbackPanel, + logFeedbackVote, + onClick: () => { + // Approve handler: apply the proposed workflow + const proposal = pendingProposalRef.current; + if (proposal && proposal.conversationItemId === responseId) { + onWorkflowProposed?.(proposal.workflow); + setConversation((current) => + current.map((item) => + item.id === responseId + ? { + ...item, + undoStatus: UndoStatus.UndoAvailable, + onClick: () => { + // After approval, clicking again will undo + if (proposal) { + onWorkflowProposed?.(proposal.previousWorkflow); + pendingProposalRef.current = null; + setConversation((c) => + c.map((i) => + i.id === responseId ? { ...i, undoStatus: UndoStatus.Undone, text: intlText.workflowUndoneText } : i + ) + ); + } + }, + } + : item + ) + ); + } + }, + }, + ...current, + ]); + } + } else { + // Text-only response (question answer, etc.) + setConversation((current) => [ + { + type: ConversationItemType.Reply, + id: guid(), + date: new Date(), + text: response.text, + isMarkdownText: true, + correlationId: chatSessionId.current, + __rawRequest: requestPayload, + __rawResponse: response, + reaction: undefined, + openFeedback: openFeedbackPanel, + logFeedbackVote, + }, + ...current, + ]); + } + }, + [ + getUpdatedWorkflow, + signal, + autoApply, + onWorkflowProposed, + openFeedbackPanel, + logFeedbackVote, + intlText.workflowAppliedText, + intlText.workflowProposedText, + intlText.workflowUndoneText, + ] + ); + + const handleChatbotResponse = useCallback( + async (query: string, requestPayload: RequestData) => { + const response = await chatbotService.getCopilotResponse(query, await getUpdatedWorkflow(), signal, await getAuthToken()); + if (!isSuccessResponse(response.status)) { + throw new Error(response.statusText); + } + const queryResponse: string = response.data.properties.response; + // commenting out usage of additionalParameters until Logic Apps backend is updated to include this response property + const additionalParameters: AdditionalParametersItem = response.data.properties.additionalParameters; + setConversation((current) => [ + { + type: ConversationItemType.Reply, + id: response.data.properties.queryId, + date: new Date(), + text: queryResponse, + isMarkdownText: false, + correlationId: chatSessionId.current, + __rawRequest: requestPayload, + __rawResponse: response, + reaction: undefined, + additionalDocURL: additionalParameters?.url ?? undefined, + azureButtonCallback: + /*additionalParameters?.includes(constants.WorkflowResponseAdditionalParameters.SendToAzure)*/ queryResponse === + constants.DefaultAzureResponseCallback && openAzureCopilotPanel + ? () => openAzureCopilotPanel(query) + : undefined, + openFeedback: openFeedbackPanel, + logFeedbackVote, + }, + ...current, + ]); + }, + [chatbotService, getUpdatedWorkflow, signal, getAuthToken, openAzureCopilotPanel, openFeedbackPanel, logFeedbackVote] + ); + const onSubmitInputQuery = useCallback( async (input: string) => { const query = input.trim(); @@ -209,35 +427,11 @@ export const CoPilotChatbot = ({ }; stopAnswerGeneration(false); try { - const response = await chatbotService.getCopilotResponse(query, await getUpdatedWorkflow(), signal, await getAuthToken()); - if (!isSuccessResponse(response.status)) { - throw new Error(response.statusText); + if (workflowEditorEnabled) { + await handleWorkflowEditResponse(query, requestPayload); + } else { + await handleChatbotResponse(query, requestPayload); } - const queryResponse: string = response.data.properties.response; - // commenting out usage of additionalParameters until Logic Apps backend is updated to include this response property - const additionalParameters: AdditionalParametersItem = response.data.properties.additionalParameters; - setConversation((current) => [ - { - type: ConversationItemType.Reply, - id: response.data.properties.queryId, - date: new Date(), - text: queryResponse, - isMarkdownText: false, - correlationId: chatSessionId.current, - __rawRequest: requestPayload, - __rawResponse: response, - reaction: undefined, - additionalDocURL: additionalParameters?.url ?? undefined, - azureButtonCallback: - /*additionalParameters?.includes(constants.WorkflowResponseAdditionalParameters.SendToAzure)*/ queryResponse === - constants.DefaultAzureResponseCallback && openAzureCopilotPanel - ? () => openAzureCopilotPanel(query) - : undefined, - openFeedback: openFeedbackPanel, - logFeedbackVote, - }, - ...current, - ]); stopAnswerGeneration(true); setTimeout(() => { setFocus(true); @@ -294,10 +488,9 @@ export const CoPilotChatbot = ({ }, [ getUpdatedWorkflow, - chatbotService, - signal, - getAuthToken, - openAzureCopilotPanel, + workflowEditorEnabled, + handleWorkflowEditResponse, + handleChatbotResponse, openFeedbackPanel, logFeedbackVote, intlText.cancelGenerationText, diff --git a/libs/designer-ui/src/lib/chatbot/chatbot.less b/libs/designer-ui/src/lib/chatbot/chatbot.less index 2cb0a996975..718b520ccf8 100644 --- a/libs/designer-ui/src/lib/chatbot/chatbot.less +++ b/libs/designer-ui/src/lib/chatbot/chatbot.less @@ -47,6 +47,7 @@ } .msla-bubble-footer { + margin-top: 8px; gap: 4px; flex-direction: column; } @@ -108,26 +109,11 @@ } } -.msla-bubble-actions-footer { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - .msla-bubble-reactions { flex-grow: 2; text-align: right; } -.msla-copy-button { - color: #323130; -} - -.msla-copy-button:hover { - color: #323130; -} - .msla-assistant-error-container { border: 1px solid rgb(237, 235, 233); border-radius: 10px; @@ -451,8 +437,4 @@ .msla-bubble-footer-role { color: #a9a9a9; } - - .msla-copy-button { - color: #f3f2f1; - } } diff --git a/libs/designer-ui/src/lib/chatbot/components/assistantError.tsx b/libs/designer-ui/src/lib/chatbot/components/assistantError.tsx index e08e3a52331..610c4234f44 100644 --- a/libs/designer-ui/src/lib/chatbot/components/assistantError.tsx +++ b/libs/designer-ui/src/lib/chatbot/components/assistantError.tsx @@ -1,8 +1,8 @@ import { useFeedbackMessage, useReportBugButton } from '../feedbackHelper'; +import type { ChatBubbleAction } from './chatBubble'; import { ChatBubble } from './chatBubble'; import type { AssistantErrorItem } from './conversationItem'; import { TechnicalErrorMessage } from './technicalErrorMessage'; -import type { IButtonProps } from '@fluentui/react'; import { useIntl } from 'react-intl'; type AssistantErrorProps = { @@ -41,7 +41,7 @@ export const AssistantError = ({ item }: AssistantErrorProps) => { }), }; - const additionalFooterActions: IButtonProps[] | undefined = []; + const additionalFooterActions: ChatBubbleAction[] = []; additionalFooterActions.push(reportBugButton); return ( diff --git a/libs/designer-ui/src/lib/chatbot/components/assistantGreeting.tsx b/libs/designer-ui/src/lib/chatbot/components/assistantGreeting.tsx index 1508a300a9e..943e9b41224 100644 --- a/libs/designer-ui/src/lib/chatbot/components/assistantGreeting.tsx +++ b/libs/designer-ui/src/lib/chatbot/components/assistantGreeting.tsx @@ -15,14 +15,21 @@ export const AssistantGreeting = ({ item }: { item: AssistantGreetingItem }) => id: 'Yuxprm', description: 'Chatbot greeting message from existing flow', }), - subHeading1: intl.formatMessage({ - defaultMessage: `This assistant can help you learn about your workflows and Azure Logic Apps platform's capabilities and connectors.`, - id: 'eO1h/h', - description: 'Chatbot introduction message to suggest what it can help with', - }), + subHeading1: item.workflowEditingEnabled + ? intl.formatMessage({ + defaultMessage: + 'This assistant can help you learn about your workflows, answer questions about Azure Logic Apps, and make changes to your workflow.', + id: '1NVeRR', + description: 'Chatbot introduction message when workflow editing is enabled', + }) + : intl.formatMessage({ + defaultMessage: `This assistant can help you learn about your workflows and Azure Logic Apps platform's capabilities and connectors.`, + id: 'eO1h/h', + description: 'Chatbot introduction message to suggest what it can help with', + }), subHeading2: intl.formatMessage({ - defaultMessage: 'Some things you can ask:', - id: 'kEjmTx', + defaultMessage: 'Some things you can try:', + id: 'hFjNA8', description: 'Chatbot introduction message to suggest what it can help with', }), suggestedPromptItem1: intl.formatMessage({ @@ -30,26 +37,44 @@ export const AssistantGreeting = ({ item }: { item: AssistantGreetingItem }) => id: 'o5fYVy', description: 'Chatbot suggestion message to describe the workflow', }), - suggestedPromptItem2: intl.formatMessage({ - defaultMessage: 'Explain how to receive files from SFTP server.', - id: 'Pnt0Xj', - description: 'Chatbot suggestion message to recieve specific files from SFTP server', - }), - suggestedPromptItem3: intl.formatMessage({ - defaultMessage: 'How can I call an external endpoint?', - id: 'NhJPUn', - description: 'Chatbot suggestion message to call an external endpoint', - }), + suggestedPromptItem2: item.workflowEditingEnabled + ? intl.formatMessage({ + defaultMessage: 'Add a response action that returns a 200 status code.', + id: 'VuvZff', + description: 'Chatbot suggestion message to add a response action to the workflow', + }) + : intl.formatMessage({ + defaultMessage: 'Explain how to receive files from SFTP server.', + id: 'Pnt0Xj', + description: 'Chatbot suggestion message to recieve specific files from SFTP server', + }), + suggestedPromptItem3: item.workflowEditingEnabled + ? intl.formatMessage({ + defaultMessage: 'Add error handling to this workflow.', + id: 'p4+r7z', + description: 'Chatbot suggestion message to add error handling to the workflow', + }) + : intl.formatMessage({ + defaultMessage: 'How can I call an external endpoint?', + id: 'NhJPUn', + description: 'Chatbot suggestion message to call an external endpoint', + }), suggestedPromptItem4: intl.formatMessage({ defaultMessage: 'What is the concurrency setting of this workflow?', id: 'WMX2ig', description: 'Chatbot suggestion message to get the concurrency setting of the workflow', }), - outroMessage: intl.formatMessage({ - defaultMessage: `The workflow assistant is designed only to provide help and doesn't support workflow creation or editing.`, - id: 'Z8tBFS', - description: 'Chatbot disclaimer message that workflow assistant can only provide help and not modify workflows', - }), + outroMessage: item.workflowEditingEnabled + ? intl.formatMessage({ + defaultMessage: 'You can ask questions or describe changes you want to make to your workflow.', + id: '1Gsrs4', + description: 'Chatbot outro message when workflow editing is enabled', + }) + : intl.formatMessage({ + defaultMessage: `The workflow assistant is designed only to provide help and doesn't support workflow creation or editing.`, + id: 'Z8tBFS', + description: 'Chatbot disclaimer message that workflow assistant can only provide help and not modify workflows', + }), }; const suggestedPrompts = [ diff --git a/libs/designer-ui/src/lib/chatbot/components/assistantReplyWithFlow.tsx b/libs/designer-ui/src/lib/chatbot/components/assistantReplyWithFlow.tsx index 1b53abb18ca..d5056e67fda 100644 --- a/libs/designer-ui/src/lib/chatbot/components/assistantReplyWithFlow.tsx +++ b/libs/designer-ui/src/lib/chatbot/components/assistantReplyWithFlow.tsx @@ -1,8 +1,10 @@ import { Confirm } from '../../dialogs/confirm'; import { useFeedbackMessage, useReportBugButton } from '../feedbackHelper'; +import type { ChatBubbleAction } from './chatBubble'; import { ChatBubble } from './chatBubble'; import { UndoStatus, type AssistantReplyWithFlowItem } from './conversationItem'; import { FlowDiffPreview } from './flowDiffPreview'; +import { ArrowUndoRegular } from '@fluentui/react-icons'; import React from 'react'; import { useIntl } from 'react-intl'; @@ -67,12 +69,12 @@ export const AssistantReplyWithFlow: React.FC = ({ setIsUndoConfirmationOpen(false); }, []); - const additionalFooterActions = []; + const additionalFooterActions: ChatBubbleAction[] = []; if (item.undoStatus === UndoStatus.UndoAvailable) { additionalFooterActions.push({ text: intlText.undo, onClick: () => setIsUndoConfirmationOpen(true), - iconProps: { iconName: 'Undo' }, + iconElement: React.createElement(ArrowUndoRegular), disabled: false, // TODO }); } else if (item.undoStatus === UndoStatus.Undone) { diff --git a/libs/designer-ui/src/lib/chatbot/components/chatBubble.tsx b/libs/designer-ui/src/lib/chatbot/components/chatBubble.tsx index dbf79f8591b..84e008c56d5 100644 --- a/libs/designer-ui/src/lib/chatbot/components/chatBubble.tsx +++ b/libs/designer-ui/src/lib/chatbot/components/chatBubble.tsx @@ -1,11 +1,9 @@ -import constants from '../constants'; import { animations } from './animations'; import { ThumbsReactionButton } from './thumbsReactionButton'; -import { ActionButton, IconButton, css, useTheme } from '@fluentui/react'; -import type { IButtonProps, IButtonStyles } from '@fluentui/react'; -import { Link } from '@fluentui/react-components'; -import { useConst } from '@fluentui/react-hooks'; +import { Button, Link, mergeClasses, Tooltip } from '@fluentui/react-components'; +import { bundleIcon, CopyFilled, CopyRegular } from '@fluentui/react-icons'; import type React from 'react'; +import { useMemo } from 'react'; import { useIntl } from 'react-intl'; export const ChatEntryReaction = { @@ -14,6 +12,16 @@ export const ChatEntryReaction = { } as const; export type ChatEntryReaction = (typeof ChatEntryReaction)[keyof typeof ChatEntryReaction]; +export type ChatBubbleAction = { + text?: string; + onClick?: () => void; + disabled?: boolean; + iconName?: string; + iconElement?: React.ReactElement; +}; + +const CopyIcon = bundleIcon(CopyFilled, CopyRegular); + type ChatBubbleProps = { isUserMessage?: boolean; children: any; @@ -22,7 +30,7 @@ type ChatBubbleProps = { hideFooter?: boolean; isEmphasized?: boolean; additionalLinksSection?: JSX.Element; - additionalFooterActions?: IButtonProps[]; + additionalFooterActions?: ChatBubbleAction[]; className?: string; selectedReaction?: ChatEntryReaction; onThumbsReactionClicked?: (reaction: ChatEntryReaction) => void; @@ -50,15 +58,14 @@ export const ChatBubble: React.FC = ({ isEmphasized, textRef, }) => { - const copyDisabled = useConst(() => { + const copyDisabled = useMemo(() => { try { return !document.queryCommandSupported('Copy'); } catch { return true; } - }); + }, []); const intl = useIntl(); - const { isInverted } = useTheme(); const intlText = { aIGeneratedDisclaimer: intl.formatMessage({ defaultMessage: 'AI-generated content may be incorrect', @@ -84,7 +91,7 @@ export const ChatBubble: React.FC = ({ return (
= ({ className )} > -
{children}
+
{children}
{additionalLinksSection && additionalLinksSection} {(additionalFooterActions && additionalFooterActions.length > 0) || (isAIGenerated && !hideFooter) ? (
{additionalFooterActions && (
{additionalFooterActions.map((action, i) => ( -
- -
+ ))}
)} @@ -109,13 +116,9 @@ export const ChatBubble: React.FC = ({
{intlText.aIGeneratedDisclaimer}
{textRef && ( - + +