diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx index 07c4f97c2d..b2f8e4d23b 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx @@ -1,4 +1,5 @@ import { RUN_EVALUATION_INPUT } from '@/ai/graphql/mutations/runEvaluationInput'; +import { GET_AGENT_TURNS } from '@/ai/graphql/queries/getAgentTurns'; import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; @@ -8,10 +9,14 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { IconDotsVertical, IconMessage, @@ -23,6 +28,7 @@ import { Button, LightIconButton } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { MenuItem } from 'twenty-ui/navigation'; import { v4 as uuidv4 } from 'uuid'; +import { SETTINGS_AGENT_DETAIL_TABS } from '../constants/SettingsAgentDetailTabs'; const DELETE_EVAL_INPUT_MODAL_ID = 'delete-eval-input-modal'; @@ -60,19 +66,28 @@ export const SettingsAgentEvalsTab = ({ const [inputToDelete, setInputToDelete] = useState(null); const { openModal } = useModal(); const { closeDropdown } = useCloseDropdown(); - const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); + const navigate = useNavigate(); + + const tabListComponentId = `${SETTINGS_AGENT_DETAIL_TABS.COMPONENT_INSTANCE_ID}-${agentId}`; + const setActiveTabId = useSetRecoilComponentState( + activeTabIdComponentState, + tabListComponentId, + ); const [runEvaluationInput] = useMutation(RUN_EVALUATION_INPUT, { onCompleted: () => { - enqueueSuccessSnackBar({ - message: t`Evaluation input executed successfully`, - }); + const logsTabId = SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.LOGS; + setActiveTabId(logsTabId); + navigate(`#${logsTabId}`); }, onError: () => { enqueueErrorSnackBar({ message: t`Failed to execute evaluation input`, }); }, + refetchQueries: [getOperationName(GET_AGENT_TURNS) ?? ''], + awaitRefetchQueries: false, }); const evalInputs: EvalInput[] = evaluationInputs.map((text) => ({ diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentLogsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentLogsTab.tsx index 6563f78591..507dd120e6 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentLogsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentLogsTab.tsx @@ -3,12 +3,12 @@ import { Table } from '@/ui/layout/table/components/Table'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { useMutation, useQuery } from '@apollo/client'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; +import { useState } from 'react'; import Skeleton from 'react-loading-skeleton'; import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { IconChevronRight, Status } from 'twenty-ui/display'; import { Button, LightIconButton } from 'twenty-ui/input'; import { @@ -19,8 +19,10 @@ import { AnimatedPlaceholderEmptyTitle, } from 'twenty-ui/layout'; import { UndecoratedLink } from 'twenty-ui/navigation'; -import { EVALUATE_AGENT_TURN } from '@/ai/graphql/mutations/evaluateAgentTurn'; -import { GET_AGENT_TURNS } from '@/ai/graphql/queries/getAgentTurns'; +import { + useEvaluateAgentTurnMutation, + useGetAgentTurnsQuery, +} from '~/generated-metadata/graphql'; const StyledTable = styled(Table)` margin-top: ${({ theme }) => theme.spacing(3)}; @@ -63,31 +65,10 @@ export const SettingsAgentLogsTab = ({ agentId, }: SettingsAgentLogsTabProps) => { const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); - - const { data, loading, refetch } = useQuery(GET_AGENT_TURNS, { - variables: { agentId }, - skip: !agentId, - }); - - const [evaluateTurn, { loading: evaluating }] = useMutation( - EVALUATE_AGENT_TURN, - { - onCompleted: () => { - enqueueSuccessSnackBar({ - message: t`Turn evaluated successfully`, - }); - refetch(); - }, - onError: () => { - enqueueErrorSnackBar({ - message: t`Failed to evaluate turn`, - }); - }, - }, + const [evaluatingTurnIds, setEvaluatingTurnIds] = useState>( + new Set(), ); - const turns = data?.agentTurns || []; - const getLatestEvaluation = (evaluations: any[]) => { if (!evaluations || evaluations.length === 0) return null; return [...evaluations].sort( @@ -96,6 +77,73 @@ export const SettingsAgentLogsTab = ({ )[0]; }; + const computeBackgroundEvaluatingTurnIds = (turnsData: any[]) => { + const now = Date.now(); + const RECENT_TURN_THRESHOLD = 5 * 60 * 1000; + const backgroundEvaluatingTurnIds = new Set(); + + turnsData.forEach((turn: any) => { + const latestEvaluation = getLatestEvaluation(turn.evaluations); + const turnAge = now - new Date(turn.createdAt).getTime(); + + if (!isDefined(latestEvaluation) && turnAge < RECENT_TURN_THRESHOLD) { + backgroundEvaluatingTurnIds.add(turn.id); + } + }); + + return backgroundEvaluatingTurnIds; + }; + + const { data, loading, refetch, startPolling, stopPolling } = + useGetAgentTurnsQuery({ + variables: { agentId }, + skip: !agentId, + onCompleted: (completedData) => { + const backgroundIds = computeBackgroundEvaluatingTurnIds( + completedData?.agentTurns || [], + ); + if (backgroundIds.size > 0) { + startPolling(3000); + } else { + stopPolling(); + } + }, + }); + + const turns = data?.agentTurns || []; + const backgroundEvaluatingTurnIds = computeBackgroundEvaluatingTurnIds(turns); + + const [evaluateTurn, { loading: evaluating }] = useEvaluateAgentTurnMutation({ + onCompleted: (data) => { + const turnId = data?.evaluateAgentTurn?.turnId; + if (isDefined(turnId)) { + setEvaluatingTurnIds((prev) => { + const next = new Set(prev); + next.delete(turnId); + return next; + }); + } + enqueueSuccessSnackBar({ + message: t`Turn evaluated successfully`, + }); + refetch(); + }, + }); + + const handleEvaluateTurn = (turnId: string) => { + setEvaluatingTurnIds((prev) => new Set(prev).add(turnId)); + evaluateTurn({ variables: { turnId } }).catch(() => { + setEvaluatingTurnIds((prev) => { + const next = new Set(prev); + next.delete(turnId); + return next; + }); + enqueueErrorSnackBar({ + message: t`Failed to evaluate turn`, + }); + }); + }; + const getScoreColor = (score: number) => { if (score >= 80) return 'green'; if (score >= 60) return 'orange'; @@ -173,13 +221,14 @@ export const SettingsAgentLogsTab = ({ color={getScoreColor(latestEvaluation.score)} text={`${latestEvaluation.score}`} /> + ) : evaluatingTurnIds.has(turn.id) || + backgroundEvaluatingTurnIds.has(turn.id) ? ( + ) : (