From 17bbfe9aff0ba2bea4fe10cdfe3c40fb1aba872d Mon Sep 17 00:00:00 2001 From: andrew-eldridge Date: Thu, 12 Mar 2026 15:14:40 -0400 Subject: [PATCH 1/3] initial evaluations changes for designer --- .../DesignerCommandBarV2.tsx | 16 + .../AzureLogicAppsDesigner/laDesignerV2.tsx | 31 +- libs/designer-v2/src/lib/core/index.ts | 7 + .../src/lib/core/queries/evaluations.ts | 154 ++++++++ .../designerOptionsInterfaces.ts | 2 + .../state/evaluation/evaluationInterfaces.ts | 35 ++ .../state/evaluation/evaluationSelectors.ts | 45 +++ .../core/state/evaluation/evaluationSlice.ts | 129 +++++++ libs/designer-v2/src/lib/core/store.ts | 2 + .../lib/ui/evaluation/EvaluateView.styles.ts | 351 ++++++++++++++++++ .../src/lib/ui/evaluation/EvaluateView.tsx | 127 +++++++ .../ui/evaluation/EvaluationResultPanel.tsx | 108 ++++++ .../lib/ui/evaluation/EvaluatorFormPanel.tsx | 345 +++++++++++++++++ .../lib/ui/evaluation/EvaluatorViewPanel.tsx | 263 +++++++++++++ .../src/lib/ui/evaluation/EvaluatorsPanel.tsx | 219 +++++++++++ .../src/lib/ui/evaluation/RunDatasetPanel.tsx | 192 ++++++++++ .../lib/ui/evaluation/evaluatorFormHelpers.ts | 139 +++++++ libs/designer-v2/src/lib/ui/index.tsx | 1 + .../src/designer-client-services/index.ts | 1 + .../lib/evaluation.ts | 34 ++ .../src/utils/src/lib/models/evaluation.ts | 53 +++ .../src/utils/src/lib/models/index.ts | 1 + 22 files changed, 2254 insertions(+), 1 deletion(-) create mode 100644 libs/designer-v2/src/lib/core/queries/evaluations.ts create mode 100644 libs/designer-v2/src/lib/core/state/evaluation/evaluationInterfaces.ts create mode 100644 libs/designer-v2/src/lib/core/state/evaluation/evaluationSelectors.ts create mode 100644 libs/designer-v2/src/lib/core/state/evaluation/evaluationSlice.ts create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluateView.styles.ts create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluateView.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluationResultPanel.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluatorFormPanel.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluatorViewPanel.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/EvaluatorsPanel.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/RunDatasetPanel.tsx create mode 100644 libs/designer-v2/src/lib/ui/evaluation/evaluatorFormHelpers.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/evaluation.ts create mode 100644 libs/logic-apps-shared/src/utils/src/lib/models/evaluation.ts diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBarV2.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBarV2.tsx index 0499be37aaf..80f23ea1a07 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBarV2.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBarV2.tsx @@ -120,6 +120,7 @@ export const DesignerCommandBar = ({ isDesignerView, isMonitoringView, isCodeView, + isEvaluateView, isDarkMode, isUnitTest, isDraftMode, @@ -127,6 +128,7 @@ export const DesignerCommandBar = ({ showMonitoringView, showDesignerView, showCodeView, + showEvaluateView, switchWorkflowMode, }: { id: string; @@ -143,6 +145,7 @@ export const DesignerCommandBar = ({ isDesignerView?: boolean; isMonitoringView?: boolean; isCodeView?: boolean; + isEvaluateView?: boolean; isDarkMode: boolean; isUnitTest: boolean; isDraftMode?: boolean; @@ -151,6 +154,7 @@ export const DesignerCommandBar = ({ showMonitoringView: () => void; showDesignerView: () => void; showCodeView: () => void; + showEvaluateView: () => void; switchWorkflowMode: (draftMode: boolean) => void; }) => { const styles = useStyles(); @@ -348,6 +352,18 @@ export const DesignerCommandBar = ({ > Run history + ); diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerV2.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerV2.tsx index ee72ceebe32..c5e178aca74 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerV2.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerV2.tsx @@ -75,6 +75,7 @@ import { setIsWorkflowDirty, setFocusNode, changePanelNode, + EvaluateView, } from '@microsoft/logic-apps-designer-v2'; import axios from 'axios'; import isEqual from 'lodash.isequal'; @@ -130,6 +131,7 @@ const DesignerEditor = () => { const [workflow, setWorkflow] = useState(); // Current workflow on the designer const [isDesignerView, setIsDesignerView] = useState(true); const [isCodeView, setIsCodeView] = useState(false); + const [isEvaluateView, setIsEvaluateView] = useState(false); const [isDraftMode, setIsDraftMode] = useState(true); const codeEditorRef = useRef<{ getValue: () => string | undefined; hasChanges: () => boolean }>(null); @@ -535,6 +537,12 @@ const DesignerEditor = () => { return; } + if (isEvaluateView) { + setIsEvaluateView(false); + setIsCodeView(true); + return; + } + if (isMonitoringView) { hideMonitoringView(); setIsCodeView(true); @@ -551,6 +559,12 @@ const DesignerEditor = () => { return; } + if (isEvaluateView) { + setIsEvaluateView(false); + setIsDesignerView(true); + return; + } + if (isMonitoringView) { hideMonitoringView(); setIsDesignerView(true); @@ -579,6 +593,18 @@ const DesignerEditor = () => { } }; + const showEvaluateView = useCallback(() => { + if (isEvaluateView) { + return; + } + if (isMonitoringView) { + hideMonitoringView(); + } + setIsDesignerView(false); + setIsCodeView(false); + setIsEvaluateView(true); + }, [isEvaluateView, isMonitoringView, hideMonitoringView]); + // Our iframe root element is given a strange padding (not in this repo), this removes it useEffect(() => { const root = document.getElementById('root'); @@ -685,11 +711,13 @@ const DesignerEditor = () => { isMonitoringView={isMonitoringView} isDesignerView={isDesignerView} isCodeView={isCodeView} + isEvaluateView={isEvaluateView} enableCopilot={() => dispatch(setIsChatBotEnabled(!showChatBot))} saveWorkflowFromCode={saveWorkflowFromCode} showMonitoringView={showMonitoringView} showDesignerView={showDesignerView} showCodeView={showCodeView} + showEvaluateView={showEvaluateView} switchWorkflowMode={switchWorkflowMode} isDraftMode={isDraftMode} prodWorkflow={artifactData?.properties.files[Artifact.WorkflowFile]} @@ -736,7 +764,7 @@ const DesignerEditor = () => { }} /> - {!isCodeView && ( + {!isCodeView && !isEvaluateView && (
{
)} {isCodeView && } + {isEvaluateView && } diff --git a/libs/designer-v2/src/lib/core/index.ts b/libs/designer-v2/src/lib/core/index.ts index c907fb87d64..1a08c3293cb 100644 --- a/libs/designer-v2/src/lib/core/index.ts +++ b/libs/designer-v2/src/lib/core/index.ts @@ -129,3 +129,10 @@ export { setLocation, setSubscription, setResourceGroup } from './state/template export { getConsumptionWorkflowPayloadForCreate } from './templates/utils/createhelper'; export * from './state/modal/modalSelectors'; export * from './state/modal/modalSlice'; +export * from './queries/evaluations'; +export { + setSelectedRun as setEvaluationSelectedRun, + setSelectedAction as setEvaluationSelectedAction, + resetEvaluationState, +} from './state/evaluation/evaluationSlice'; +export type { AgentAction, WorkflowRunEntry } from './state/evaluation/evaluationInterfaces'; diff --git a/libs/designer-v2/src/lib/core/queries/evaluations.ts b/libs/designer-v2/src/lib/core/queries/evaluations.ts new file mode 100644 index 00000000000..0492cc46551 --- /dev/null +++ b/libs/designer-v2/src/lib/core/queries/evaluations.ts @@ -0,0 +1,154 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { EvaluationService } from '@microsoft/logic-apps-shared'; +import type { Evaluator } from '@microsoft/logic-apps-shared'; + +const queryOpts = { + cacheTime: 1000 * 60 * 5, + refetchOnMount: true, + refetchOnWindowFocus: false, + refetchOnReconnect: false, +}; + +export const evaluationQueryKeys = { + evaluators: 'evaluators', + evaluator: 'evaluator', + evaluationsForRun: 'evaluationsForRun', + evaluation: 'evaluation', + evaluationsForAction: 'evaluationsForAction', + evaluationForAction: 'evaluationForAction', +}; + +export const useEvaluatorsQuery = (workflowName: string, enabled = true) => { + return useQuery( + [evaluationQueryKeys.evaluators, workflowName], + async () => { + return EvaluationService().getEvaluators(workflowName); + }, + { + ...queryOpts, + enabled: enabled && !!workflowName.trim(), + } + ); +}; + +export const useEvaluatorQuery = (workflowName: string, evaluatorName: string, enabled = true) => { + return useQuery( + [evaluationQueryKeys.evaluator, workflowName, evaluatorName], + async () => { + return EvaluationService().getEvaluator(workflowName, evaluatorName); + }, + { + ...queryOpts, + enabled: enabled && !!workflowName.trim() && !!evaluatorName.trim(), + } + ); +}; + +export const useCreateOrUpdateEvaluator = (workflowName: string) => { + const queryClient = useQueryClient(); + return useMutation( + async ({ evaluatorName, evaluator }: { evaluatorName: string; evaluator: Evaluator }) => { + return EvaluationService().createOrUpdateEvaluator(workflowName, evaluatorName, evaluator); + }, + { + onSuccess: () => { + queryClient.invalidateQueries([evaluationQueryKeys.evaluators, workflowName]); + }, + } + ); +}; + +export const useDeleteEvaluator = (workflowName: string) => { + const queryClient = useQueryClient(); + return useMutation( + async (evaluatorName: string) => { + return EvaluationService().deleteEvaluator(workflowName, evaluatorName); + }, + { + onSuccess: () => { + queryClient.invalidateQueries([evaluationQueryKeys.evaluators, workflowName]); + }, + } + ); +}; + +export const useEvaluationsForRun = (workflowName: string, runId: string, enabled = true) => { + return useQuery( + [evaluationQueryKeys.evaluationsForRun, workflowName, runId], + async () => { + return EvaluationService().getEvaluationsForRun(workflowName, runId); + }, + { + ...queryOpts, + enabled: enabled && !!workflowName.trim() && !!runId.trim(), + } + ); +}; + +export const useEvaluationQuery = (workflowName: string, runId: string, evaluatorName: string, enabled = true) => { + return useQuery( + [evaluationQueryKeys.evaluation, workflowName, runId, evaluatorName], + async () => { + return EvaluationService().getEvaluation(workflowName, runId, evaluatorName); + }, + { + ...queryOpts, + enabled: enabled && !!workflowName.trim() && !!runId.trim() && !!evaluatorName.trim(), + } + ); +}; + +export const useEvaluationForActionQuery = ( + workflowName: string, + runId: string, + agentActionName: string, + evaluatorName: string, + enabled = true +) => { + return useQuery( + [evaluationQueryKeys.evaluationForAction, workflowName, runId, agentActionName, evaluatorName], + async () => { + return EvaluationService().getEvaluationForAction(workflowName, runId, agentActionName, evaluatorName); + }, + { + ...queryOpts, + enabled: enabled && !!workflowName.trim() && !!runId.trim() && !!agentActionName.trim() && !!evaluatorName.trim(), + } + ); +}; + +export const useRunEvaluation = (workflowName: string) => { + const queryClient = useQueryClient(); + return useMutation( + async ({ runId, evaluatorName }: { runId: string; evaluatorName: string }) => { + return EvaluationService().runEvaluation(workflowName, runId, evaluatorName); + }, + { + onSuccess: (_data, variables) => { + queryClient.invalidateQueries([evaluationQueryKeys.evaluationsForRun, workflowName, variables.runId]); + queryClient.invalidateQueries([evaluationQueryKeys.evaluation, workflowName, variables.runId, variables.evaluatorName]); + }, + } + ); +}; + +export const useRunEvaluationForAction = (workflowName: string) => { + const queryClient = useQueryClient(); + return useMutation( + async ({ runId, agentActionName, evaluatorName }: { runId: string; agentActionName: string; evaluatorName: string }) => { + return EvaluationService().runEvaluationForAction(workflowName, runId, agentActionName, evaluatorName); + }, + { + onSuccess: (_data, variables) => { + queryClient.invalidateQueries([evaluationQueryKeys.evaluationsForAction, workflowName, variables.runId, variables.agentActionName]); + queryClient.invalidateQueries([ + evaluationQueryKeys.evaluationForAction, + workflowName, + variables.runId, + variables.agentActionName, + variables.evaluatorName, + ]); + }, + } + ); +}; diff --git a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts index b2a7500c2fd..09d743f7417 100644 --- a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts +++ b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts @@ -24,6 +24,7 @@ import type { IUserPreferenceService, IExperimentationService, ICognitiveServiceService, + IEvaluationService, } from '@microsoft/logic-apps-shared'; import type { MaximumWaitingRunsMetadata } from '../../../ui/settings'; @@ -82,4 +83,5 @@ export interface ServiceOptions { userPreferenceService?: IUserPreferenceService; experimentationService?: IExperimentationService; cognitiveServiceService?: ICognitiveServiceService; + evaluationService?: IEvaluationService; } diff --git a/libs/designer-v2/src/lib/core/state/evaluation/evaluationInterfaces.ts b/libs/designer-v2/src/lib/core/state/evaluation/evaluationInterfaces.ts new file mode 100644 index 00000000000..eb78bff516d --- /dev/null +++ b/libs/designer-v2/src/lib/core/state/evaluation/evaluationInterfaces.ts @@ -0,0 +1,35 @@ +import type { Evaluator, EvaluationResult } from '@microsoft/logic-apps-shared'; + +export type RightPanelView = 'empty' | 'create' | 'edit' | 'view' | 'result'; + +export interface AgentAction { + name: string; + status: string; + startTime: string; + endTime: string; +} + +export interface WorkflowRunEntry { + id: string; + name: string; + startTime: string; + endTime: string; + status: string; +} + +export interface EvaluationState { + evaluators: Evaluator[]; + evaluatorsLoading: boolean; + selectedEvaluator: Evaluator | null; + selectedRun: WorkflowRunEntry | null; + selectedAction: AgentAction | null; + agentActions: AgentAction[]; + agentActionsLoading: boolean; + rightPanelView: RightPanelView; + editingEvaluator: Evaluator | null; + evaluationResult: EvaluationResult | null; + evaluationLoading: boolean; + evaluationError: string | null; + runningEvaluatorName: string; + searchQuery: string; +} diff --git a/libs/designer-v2/src/lib/core/state/evaluation/evaluationSelectors.ts b/libs/designer-v2/src/lib/core/state/evaluation/evaluationSelectors.ts new file mode 100644 index 00000000000..09dd3e98f19 --- /dev/null +++ b/libs/designer-v2/src/lib/core/state/evaluation/evaluationSelectors.ts @@ -0,0 +1,45 @@ +import type { RootState } from '../../store'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; + +export const useEvaluators = () => useSelector((state: RootState) => state.evaluation.evaluators); +export const useEvaluatorsLoading = () => useSelector((state: RootState) => state.evaluation.evaluatorsLoading); +export const useSelectedEvaluator = () => useSelector((state: RootState) => state.evaluation.selectedEvaluator); +export const useSelectedRun = () => useSelector((state: RootState) => state.evaluation.selectedRun); +export const useSelectedAction = () => useSelector((state: RootState) => state.evaluation.selectedAction); +export const useAgentActions = () => useSelector((state: RootState) => state.evaluation.agentActions); +export const useAgentActionsLoading = () => useSelector((state: RootState) => state.evaluation.agentActionsLoading); +export const useRightPanelView = () => useSelector((state: RootState) => state.evaluation.rightPanelView); +export const useEditingEvaluator = () => useSelector((state: RootState) => state.evaluation.editingEvaluator); +export const useEvaluationResult = () => useSelector((state: RootState) => state.evaluation.evaluationResult); +export const useEvaluationLoading = () => useSelector((state: RootState) => state.evaluation.evaluationLoading); +export const useEvaluationError = () => useSelector((state: RootState) => state.evaluation.evaluationError); +export const useRunningEvaluatorName = () => useSelector((state: RootState) => state.evaluation.runningEvaluatorName); +export const useSearchQuery = () => useSelector((state: RootState) => state.evaluation.searchQuery); + +export const useFilteredEvaluators = () => { + const evaluators = useEvaluators(); + const searchQuery = useSearchQuery(); + return useMemo( + () => + evaluators.filter( + (ev) => ev.name.toLowerCase().includes(searchQuery.toLowerCase()) || ev.template.toLowerCase().includes(searchQuery.toLowerCase()) + ), + [evaluators, searchQuery] + ); +}; + +export const useCanRunEvaluation = () => { + const selectedRun = useSelectedRun(); + const selectedAction = useSelectedAction(); + const workflowKind = useSelector((state: RootState) => state.workflow.workflowKind); + return useMemo(() => { + if (!selectedRun) { + return false; + } + if (workflowKind === 'stateful') { + return !!selectedAction; + } + return true; + }, [selectedRun, selectedAction, workflowKind]); +}; diff --git a/libs/designer-v2/src/lib/core/state/evaluation/evaluationSlice.ts b/libs/designer-v2/src/lib/core/state/evaluation/evaluationSlice.ts new file mode 100644 index 00000000000..727c12fc072 --- /dev/null +++ b/libs/designer-v2/src/lib/core/state/evaluation/evaluationSlice.ts @@ -0,0 +1,129 @@ +import type { Evaluator, EvaluationResult } from '@microsoft/logic-apps-shared'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import { resetWorkflowState } from '../global'; +import type { EvaluationState, RightPanelView, WorkflowRunEntry, AgentAction } from './evaluationInterfaces'; + +export const initialEvaluationState: EvaluationState = { + evaluators: [], + evaluatorsLoading: false, + selectedEvaluator: null, + selectedRun: null, + selectedAction: null, + agentActions: [], + agentActionsLoading: false, + rightPanelView: 'empty', + editingEvaluator: null, + evaluationResult: null, + evaluationLoading: false, + evaluationError: null, + runningEvaluatorName: '', + searchQuery: '', +}; + +export const evaluationSlice = createSlice({ + name: 'evaluation', + initialState: initialEvaluationState, + reducers: { + setEvaluators: (state, action: PayloadAction) => { + state.evaluators = action.payload; + }, + setEvaluatorsLoading: (state, action: PayloadAction) => { + state.evaluatorsLoading = action.payload; + }, + setSelectedEvaluator: (state, action: PayloadAction) => { + state.selectedEvaluator = action.payload; + if (action.payload) { + state.rightPanelView = 'view'; + } else { + state.rightPanelView = 'empty'; + } + }, + setSelectedRun: (state, action: PayloadAction) => { + state.selectedRun = action.payload; + state.selectedAction = null; + }, + setSelectedAction: (state, action: PayloadAction) => { + state.selectedAction = action.payload; + }, + setAgentActions: (state, action: PayloadAction) => { + state.agentActions = action.payload; + }, + setAgentActionsLoading: (state, action: PayloadAction) => { + state.agentActionsLoading = action.payload; + }, + setRightPanelView: (state, action: PayloadAction) => { + state.rightPanelView = action.payload; + }, + setEditingEvaluator: (state, action: PayloadAction) => { + state.editingEvaluator = action.payload; + }, + setEvaluationResult: (state, action: PayloadAction) => { + state.evaluationResult = action.payload; + }, + setEvaluationLoading: (state, action: PayloadAction) => { + state.evaluationLoading = action.payload; + }, + setEvaluationError: (state, action: PayloadAction) => { + state.evaluationError = action.payload; + }, + setRunningEvaluatorName: (state, action: PayloadAction) => { + state.runningEvaluatorName = action.payload; + }, + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload; + }, + startCreateEvaluator: (state) => { + state.selectedEvaluator = null; + state.editingEvaluator = null; + state.rightPanelView = 'create'; + }, + startEditEvaluator: (state, action: PayloadAction) => { + state.selectedEvaluator = action.payload; + state.editingEvaluator = action.payload; + state.rightPanelView = 'edit'; + }, + finishFormAction: (state) => { + state.rightPanelView = 'empty'; + state.editingEvaluator = null; + state.selectedEvaluator = null; + }, + cancelFormAction: (state) => { + if (state.selectedEvaluator && state.rightPanelView === 'edit') { + state.rightPanelView = 'view'; + } else { + state.rightPanelView = 'empty'; + state.selectedEvaluator = null; + } + state.editingEvaluator = null; + }, + resetEvaluationState: () => initialEvaluationState, + }, + extraReducers: (builder) => { + builder.addCase(resetWorkflowState, () => initialEvaluationState); + }, +}); + +export const { + setEvaluators, + setEvaluatorsLoading, + setSelectedEvaluator, + setSelectedRun, + setSelectedAction, + setAgentActions, + setAgentActionsLoading, + setRightPanelView, + setEditingEvaluator, + setEvaluationResult, + setEvaluationLoading, + setEvaluationError, + setRunningEvaluatorName, + setSearchQuery, + startCreateEvaluator, + startEditEvaluator, + finishFormAction, + cancelFormAction, + resetEvaluationState, +} = evaluationSlice.actions; + +export default evaluationSlice.reducer; diff --git a/libs/designer-v2/src/lib/core/store.ts b/libs/designer-v2/src/lib/core/store.ts index 9738ea030a6..1dc1c3153bf 100644 --- a/libs/designer-v2/src/lib/core/store.ts +++ b/libs/designer-v2/src/lib/core/store.ts @@ -14,6 +14,7 @@ import workflowReducer from './state/workflow/workflowSlice'; import workflowParametersReducer from './state/workflowparameters/workflowparametersSlice'; import modalReducer from './state/modal/modalSlice'; import notesReducer from './state/notes/notesSlice'; +import evaluationReducer from './state/evaluation/evaluationSlice'; import { configureStore } from '@reduxjs/toolkit'; import type {} from 'redux-thunk'; @@ -42,6 +43,7 @@ export const store = configureStore({ undoRedo: undoRedoReducer, modal: modalReducer, notes: notesReducer, + evaluation: evaluationReducer, // if is in dev environment, add devSlice to store ...(process.env.NODE_ENV === 'development' ? { dev: devReducer } : {}), }, diff --git a/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.styles.ts b/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.styles.ts new file mode 100644 index 00000000000..5aa0f5bf11d --- /dev/null +++ b/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.styles.ts @@ -0,0 +1,351 @@ +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useEvaluateViewStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + height: '100%', + overflow: 'hidden', + }, + main: { + flex: '1', + display: 'flex', + overflow: 'hidden', + }, + panel: { + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + borderRight: `1px solid ${tokens.colorNeutralStroke1}`, + }, + panelRuns: { + width: '320px', + minWidth: '240px', + }, + panelEvaluators: { + width: '420px', + minWidth: '300px', + }, + panelDetail: { + flex: '1', + minWidth: '300px', + overflow: 'auto', + borderRight: 'none', + }, + panelHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '12px 16px', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + }, + panelHeaderWithBack: { + padding: '8px 16px', + }, + panelTitle: { + margin: '0', + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + }, + searchContainer: { + padding: '8px 16px', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + }, + listContainer: { + flex: '1', + overflow: 'auto', + }, + listItem: { + padding: '10px 16px', + cursor: 'pointer', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + '&:hover': { + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + listItemSelected: { + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + runName: { + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + }, + runTiming: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + marginTop: '2px', + }, + emptyState: { + flex: '1', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + color: tokens.colorNeutralForeground2, + padding: '32px', + textAlign: 'center', + }, + emptyTitle: { + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + margin: '0 0 4px 0', + }, + emptySubtext: { + fontSize: tokens.fontSizeBase200, + margin: '0', + opacity: 0.7, + }, + loadingContainer: { + display: 'flex', + justifyContent: 'center', + padding: '24px', + }, + // Table styles for evaluators panel + tableHeader: { + display: 'flex', + padding: '8px 16px', + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + fontWeight: tokens.fontWeightSemibold, + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + }, + tableRow: { + display: 'flex', + alignItems: 'center', + padding: '8px 16px', + cursor: 'pointer', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + '&:hover': { + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + tableRowSelected: { + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + colType: { + width: '140px', + flexShrink: 0, + fontSize: tokens.fontSizeBase200, + }, + colName: { + flex: '1', + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + colActions: { + display: 'flex', + gap: '4px', + flexShrink: 0, + }, + // Agent actions list + actionItem: { + display: 'flex', + alignItems: 'center', + gap: '12px', + padding: '10px 16px', + cursor: 'pointer', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + '&:hover': { + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + actionItemSelected: { + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + actionIndex: { + width: '24px', + height: '24px', + borderRadius: '50%', + backgroundColor: tokens.colorNeutralBackground3, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + flexShrink: 0, + }, + actionDetails: { + flex: '1', + minWidth: 0, + }, + actionStatus: { + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: tokens.fontSizeBase300, + }, + actionTiming: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + marginTop: '2px', + }, + // Form styles + formContent: { + flex: '1', + overflow: 'auto', + padding: '16px', + display: 'flex', + flexDirection: 'column', + gap: '16px', + }, + formActions: { + display: 'flex', + justifyContent: 'flex-end', + gap: '8px', + padding: '12px 16px', + borderTop: `1px solid ${tokens.colorNeutralStroke1}`, + }, + formError: { + padding: '8px 12px', + margin: '0 16px', + borderRadius: '4px', + backgroundColor: tokens.colorPaletteRedBackground1, + color: tokens.colorPaletteRedForeground1, + fontSize: tokens.fontSizeBase200, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + formRow: { + display: 'flex', + gap: '12px', + }, + formFieldHalf: { + flex: '1', + }, + // Tool calls + toolCallItem: { + padding: '12px', + borderRadius: '6px', + border: `1px solid ${tokens.colorNeutralStroke1}`, + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + toolCallHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + }, + // View panel + fieldRow: { + display: 'flex', + flexDirection: 'column', + gap: '4px', + padding: '8px 0', + }, + fieldLabel: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + fontWeight: tokens.fontWeightSemibold, + }, + fieldValue: { + fontSize: tokens.fontSizeBase300, + }, + promptValue: { + whiteSpace: 'pre-wrap', + backgroundColor: tokens.colorNeutralBackground3, + padding: '8px 12px', + borderRadius: '4px', + fontSize: tokens.fontSizeBase200, + }, + definitionActions: { + display: 'flex', + gap: '8px', + paddingTop: '12px', + }, + // Result panel + resultBadge: { + display: 'inline-flex', + padding: '4px 12px', + borderRadius: '12px', + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + }, + resultPassed: { + backgroundColor: tokens.colorPaletteGreenBackground1, + color: tokens.colorPaletteGreenForeground1, + }, + resultFailed: { + backgroundColor: tokens.colorPaletteRedBackground1, + color: tokens.colorPaletteRedForeground1, + }, + tokenStats: { + display: 'flex', + gap: '16px', + padding: '12px 0', + }, + tokenStat: { + display: 'flex', + flexDirection: 'column', + gap: '2px', + }, + statLabel: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + }, + statValue: { + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + }, + resultReason: { + padding: '12px', + backgroundColor: tokens.colorNeutralBackground3, + borderRadius: '6px', + fontSize: tokens.fontSizeBase200, + whiteSpace: 'pre-wrap', + }, + // Status indicators + statusSucceeded: { + color: tokens.colorPaletteGreenForeground1, + }, + statusFailed: { + color: tokens.colorPaletteRedForeground1, + }, + statusRunning: { + color: tokens.colorPaletteYellowForeground1, + }, + // Tabs + tabContainer: { + display: 'flex', + borderBottom: `1px solid ${tokens.colorNeutralStroke1}`, + }, + // Card + card: { + padding: '16px', + borderRadius: '8px', + border: `1px solid ${tokens.colorNeutralStroke1}`, + backgroundColor: tokens.colorNeutralBackground1, + marginBottom: '16px', + }, + evaluationHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '12px', + }, + resultSection: { + padding: '8px 0', + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + detailRow: { + display: 'flex', + justifyContent: 'space-between', + padding: '4px 0', + }, + detailLabel: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground2, + }, + detailValue: { + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + }, +}); diff --git a/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.tsx b/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.tsx new file mode 100644 index 00000000000..fc973b327d1 --- /dev/null +++ b/libs/designer-v2/src/lib/ui/evaluation/EvaluateView.tsx @@ -0,0 +1,127 @@ +import { mergeClasses } from '@fluentui/react-components'; +import { useDispatch, useSelector } from 'react-redux'; +import { + useSelectedEvaluator, + useRightPanelView, + useSelectedRun, + useSelectedAction, +} from '../../core/state/evaluation/evaluationSelectors'; +import { + startEditEvaluator, + setSelectedEvaluator, + setRightPanelView, + setEvaluationLoading, + setEvaluationError, + setEvaluationResult, + setRunningEvaluatorName, +} from '../../core/state/evaluation/evaluationSlice'; +import { useRunEvaluation, useRunEvaluationForAction, useDeleteEvaluator } from '../../core/queries/evaluations'; +import type { RootState } from '../../core/store'; +import { RunDatasetPanel } from './RunDatasetPanel'; +import { EvaluatorsPanel } from './EvaluatorsPanel'; +import { EvaluatorFormPanel } from './EvaluatorFormPanel'; +import { EvaluatorViewPanel } from './EvaluatorViewPanel'; +import { EvaluationResultPanel } from './EvaluationResultPanel'; +import { useEvaluateViewStyles } from './EvaluateView.styles'; +import { useCallback } from 'react'; + +interface EvaluateViewProps { + workflowName: string; +} + +export const EvaluateView = ({ workflowName }: EvaluateViewProps) => { + const styles = useEvaluateViewStyles(); + const dispatch = useDispatch(); + const selectedEvaluator = useSelectedEvaluator(); + const rightPanelView = useRightPanelView(); + const selectedRun = useSelectedRun(); + const selectedAction = useSelectedAction(); + const workflowKind = useSelector((state: RootState) => state.workflow.workflowKind); + + const runEvaluation = useRunEvaluation(workflowName); + const runEvaluationForAction = useRunEvaluationForAction(workflowName); + const deleteEvaluator = useDeleteEvaluator(workflowName); + + const handleRunClick = useCallback(async () => { + if (!selectedRun || !selectedEvaluator) { + return; + } + + dispatch(setRightPanelView('result')); + dispatch(setEvaluationLoading(true)); + dispatch(setEvaluationError(null)); + dispatch(setEvaluationResult(null)); + dispatch(setRunningEvaluatorName(selectedEvaluator.name)); + + try { + const isStateful = workflowKind === 'stateful' || workflowKind === 'agentic'; + if (isStateful && selectedAction) { + const result = await runEvaluationForAction.mutateAsync({ + runId: selectedRun.id, + agentActionName: selectedAction.name, + evaluatorName: selectedEvaluator.name, + }); + dispatch(setEvaluationResult(result)); + } else { + const result = await runEvaluation.mutateAsync({ + runId: selectedRun.id, + evaluatorName: selectedEvaluator.name, + }); + dispatch(setEvaluationResult(result)); + } + } catch (err) { + dispatch(setEvaluationError(err instanceof Error ? err.message : 'Failed to run evaluation')); + } finally { + dispatch(setEvaluationLoading(false)); + } + }, [dispatch, selectedRun, selectedAction, selectedEvaluator, workflowKind, runEvaluation, runEvaluationForAction]); + + const handleDeleteClick = useCallback(async () => { + if (!selectedEvaluator) { + return; + } + try { + await deleteEvaluator.mutateAsync(selectedEvaluator.name); + dispatch(setSelectedEvaluator(null)); + } catch (err) { + console.error('Failed to delete evaluator:', err); + } + }, [deleteEvaluator, dispatch, selectedEvaluator]); + + const renderRightPanel = () => { + switch (rightPanelView) { + case 'create': + case 'edit': + return ; + case 'view': + return selectedEvaluator ? ( + dispatch(startEditEvaluator(selectedEvaluator))} + onRun={handleRunClick} + onDelete={handleDeleteClick} + /> + ) : null; + case 'result': + return ; + default: + return ( +
+

Select an action to get started

+

Create a new evaluator or select one to view

+
+ ); + } + }; + + return ( +
+
+ + +
{renderRightPanel()}
+
+
+ ); +}; diff --git a/libs/designer-v2/src/lib/ui/evaluation/EvaluationResultPanel.tsx b/libs/designer-v2/src/lib/ui/evaluation/EvaluationResultPanel.tsx new file mode 100644 index 00000000000..e81f77c4b0f --- /dev/null +++ b/libs/designer-v2/src/lib/ui/evaluation/EvaluationResultPanel.tsx @@ -0,0 +1,108 @@ +import { mergeClasses, Spinner } from '@fluentui/react-components'; +import { + useEvaluationResult, + useEvaluationLoading, + useEvaluationError, + useRunningEvaluatorName, +} from '../../core/state/evaluation/evaluationSelectors'; +import { useEvaluateViewStyles } from './EvaluateView.styles'; + +export const EvaluationResultPanel = () => { + const styles = useEvaluateViewStyles(); + const result = useEvaluationResult(); + const loading = useEvaluationLoading(); + const error = useEvaluationError(); + const evaluatorName = useRunningEvaluatorName(); + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + if (error) { + return ( +
+
+
+

Evaluation Result

+

Evaluator: {evaluatorName}

+
+
+
+
+ {error} +
+
+
+ ); + } + + if (!result) { + return ( +
+
+

No evaluation result

+
+
+ ); + } + + const isPassed = result.result?.toLowerCase() === 'passed'; + + return ( +
+
+
+

Evaluation Result

+

Evaluator: {evaluatorName}

+
+
+ +
+
+
+ {result.result} + + Value: + {result.value} + +
+ + {result.agentActionName && ( +
+ Agent Action + {result.agentActionName} +
+ )} + + {result.reason && ( +
+ Reason +
{result.reason}
+
+ )} + +
+
+ Total Tokens + {result.totalTokens} +
+
+ Input Tokens + {result.inputTokens} +
+
+ Output Tokens + {result.outputTokens} +
+
+
+
+
+ ); +}; diff --git a/libs/designer-v2/src/lib/ui/evaluation/EvaluatorFormPanel.tsx b/libs/designer-v2/src/lib/ui/evaluation/EvaluatorFormPanel.tsx new file mode 100644 index 00000000000..1b38018bea9 --- /dev/null +++ b/libs/designer-v2/src/lib/ui/evaluation/EvaluatorFormPanel.tsx @@ -0,0 +1,345 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Button, Dropdown, Input, Label, Option, Radio, RadioGroup, Checkbox, Textarea, Spinner } from '@fluentui/react-components'; +import { DeleteRegular, AddRegular, SaveRegular } from '@fluentui/react-icons'; +import { useDispatch } from 'react-redux'; +import { finishFormAction, cancelFormAction } from '../../core/state/evaluation/evaluationSlice'; +import { useEditingEvaluator, useRightPanelView } from '../../core/state/evaluation/evaluationSelectors'; +import { useCreateOrUpdateEvaluator } from '../../core/queries/evaluations'; +import type { EvaluatorTemplate, ComparisonMethod } from '@microsoft/logic-apps-shared'; +import type { EvaluatorFormData } from './evaluatorFormHelpers'; +import { + createDefaultEvaluatorFormData, + evaluatorToFormData, + formDataToEvaluator, + createDefaultToolCallFormItem, +} from './evaluatorFormHelpers'; +import { useEvaluateViewStyles } from './EvaluateView.styles'; + +interface EvaluatorFormPanelProps { + workflowName: string; +} + +export const EvaluatorFormPanel = ({ workflowName }: EvaluatorFormPanelProps) => { + const styles = useEvaluateViewStyles(); + const dispatch = useDispatch(); + const rightPanelView = useRightPanelView(); + const editingEvaluator = useEditingEvaluator(); + const mode = rightPanelView === 'edit' ? 'edit' : 'create'; + const createOrUpdate = useCreateOrUpdateEvaluator(workflowName); + + const [formData, setFormData] = useState(createDefaultEvaluatorFormData()); + const [error, setError] = useState(null); + + useEffect(() => { + if (mode === 'edit' && editingEvaluator) { + setFormData(evaluatorToFormData(editingEvaluator)); + } else { + setFormData(createDefaultEvaluatorFormData()); + } + }, [mode, editingEvaluator]); + + const updateFormField = useCallback((field: K, value: EvaluatorFormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async () => { + if (!formData.name.trim()) { + setError('Evaluator name is required'); + return; + } + + setError(null); + try { + const evalData = formDataToEvaluator(formData); + await createOrUpdate.mutateAsync({ evaluatorName: formData.name, evaluator: evalData }); + dispatch(finishFormAction()); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save evaluator'); + } + }, [formData, createOrUpdate, dispatch]); + + return ( +
+
+
+

{mode === 'create' ? 'Create Evaluator' : 'Edit Evaluator'}

+

+ {mode === 'create' ? 'Configure a new evaluator for your agent' : 'Update evaluator configuration'} +

+
+
+ + {error && ( +
+ {error} + +
+ )} + +
+
+ + updateFormField('name', data.value)} + placeholder="Enter evaluator name" + disabled={mode === 'edit'} + style={{ width: '100%' }} + /> +
+ +
+ + updateFormField('template', data.optionValue as EvaluatorTemplate)} + style={{ width: '100%' }} + > + + + + +
+ + {/* Model configuration - not needed for ToolCallTrajectory */} + {formData.template !== 'ToolCallTrajectory' && ( + <> +
+ + updateFormField('agentModelType', data.optionValue as string)} + style={{ width: '100%' }} + > + + + +
+ +
+ + updateFormField('deploymentId', data.value)} + placeholder="Enter deployment identifier" + style={{ width: '100%' }} + /> +
+ +
+ + updateFormField('modelReferenceName', data.value)} + placeholder="Enter model connection reference name" + style={{ width: '100%' }} + /> +
+ +
+ + updateFormField('modelName', data.value)} + placeholder="Enter deployment model name" + style={{ width: '100%' }} + /> +
+ + )} + + {/* Ground truth fields */} + {(formData.template === 'ToolCallTrajectory' || formData.template === 'SemanticSimilarity') && ( + <> + updateFormField('useGroundTruthRun', data.value === 'groundTruth')} + > + + + + + {formData.useGroundTruthRun && ( + <> +
+ + updateFormField('groundTruthRunId', data.value)} + placeholder="Enter ground truth run identifier" + style={{ width: '100%' }} + /> +
+
+ + updateFormField('groundTruthAgentActionName', data.value)} + placeholder="Enter ground truth agent action name" + style={{ width: '100%' }} + /> +
+ + )} + + )} + + {/* CustomPrompt: Instructions */} + {formData.template === 'CustomPrompt' && ( +
+ +