diff --git a/apps/api/src/app/agents/dtos/agent-behavior.dto.ts b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts index 1a13e3ff3e5..860c2db06c7 100644 --- a/apps/api/src/app/agents/dtos/agent-behavior.dto.ts +++ b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts @@ -1,19 +1,19 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, ValidateIf, ValidateNested } from 'class-validator'; -import { Type } from 'class-transformer'; +import { IsBoolean, IsOptional, ValidateIf } from 'class-validator'; import { IsWellKnownEmoji } from '../validators/is-well-known-emoji.validator'; -export class AgentReactionSettingsDto { +export class AgentBehaviorDto { @ApiPropertyOptional({ description: - 'Cross-platform emoji name for incoming messages (e.g. "eyes", "thumbs_up"). ' + - 'Set to null to disable. Default: "eyes"', - default: 'eyes', + 'Acknowledge incoming messages. On platforms that support a native typing indicator ' + + '(e.g. Slack, Microsoft Teams), shows a "Typing…" indicator while the agent processes the message. ' + + 'On platforms that do not (e.g. WhatsApp), reacts with an "eyes" emoji to the first ' + + 'inbound message in a thread. Default: true', + default: true, }) + @IsBoolean() @IsOptional() - @ValidateIf((_, value) => value !== null) - @IsWellKnownEmoji() - onMessageReceived?: string | null; + acknowledgeOnReceived?: boolean; @ApiPropertyOptional({ description: @@ -24,18 +24,5 @@ export class AgentReactionSettingsDto { @IsOptional() @ValidateIf((_, value) => value !== null) @IsWellKnownEmoji() - onResolved?: string | null; -} - -export class AgentBehaviorDto { - @ApiPropertyOptional({ description: 'Show a "Thinking..." indicator while the agent is processing a message' }) - @IsBoolean() - @IsOptional() - thinkingIndicatorEnabled?: boolean; - - @ApiPropertyOptional({ type: AgentReactionSettingsDto, description: 'Automatic emoji reactions on messages' }) - @ValidateNested() - @Type(() => AgentReactionSettingsDto) - @IsOptional() - reactions?: AgentReactionSettingsDto; + reactionOnResolved?: string | null; } diff --git a/apps/api/src/app/agents/dtos/agent-platform.enum.ts b/apps/api/src/app/agents/dtos/agent-platform.enum.ts index b1d01298bf7..19e7bc67f0c 100644 --- a/apps/api/src/app/agents/dtos/agent-platform.enum.ts +++ b/apps/api/src/app/agents/dtos/agent-platform.enum.ts @@ -4,6 +4,7 @@ export enum AgentPlatformEnum { TEAMS = 'teams', } -export const PLATFORMS_WITHOUT_TYPING_INDICATOR = new Set([ - AgentPlatformEnum.WHATSAPP, +export const PLATFORMS_WITH_TYPING_INDICATOR = new Set([ + AgentPlatformEnum.SLACK, + AgentPlatformEnum.TEAMS, ]); diff --git a/apps/api/src/app/agents/e2e/agents.e2e.ts b/apps/api/src/app/agents/e2e/agents.e2e.ts index c45c57d9df0..1b5cdc5b0b2 100644 --- a/apps/api/src/app/agents/e2e/agents.e2e.ts +++ b/apps/api/src/app/agents/e2e/agents.e2e.ts @@ -65,7 +65,7 @@ describe('Agents API - /agents #novu-v2', () => { expect(afterDelete.status).to.equal(404); }); - it('should update and return agent behavior settings', async () => { + it('should update and return acknowledgeOnReceived behavior', async () => { const identifier = `e2e-behavior-${Date.now()}`; const createRes = await session.testAgent.post('/v1/agents').send({ @@ -77,28 +77,28 @@ describe('Agents API - /agents #novu-v2', () => { expect(createRes.body.data.behavior).to.equal(undefined); const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ - behavior: { thinkingIndicatorEnabled: false }, + behavior: { acknowledgeOnReceived: false }, }); expect(patchRes.status).to.equal(200); - expect(patchRes.body.data.behavior).to.deep.equal({ thinkingIndicatorEnabled: false }); + expect(patchRes.body.data.behavior).to.deep.equal({ acknowledgeOnReceived: false }); const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`); expect(getRes.status).to.equal(200); - expect(getRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(false); + expect(getRes.body.data.behavior.acknowledgeOnReceived).to.equal(false); const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ - behavior: { thinkingIndicatorEnabled: true }, + behavior: { acknowledgeOnReceived: true }, }); expect(reEnableRes.status).to.equal(200); - expect(reEnableRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(true); + expect(reEnableRes.body.data.behavior.acknowledgeOnReceived).to.equal(true); await session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`); }); - it('should update and return agent reaction settings with defaults', async () => { + it('should update and return reactionOnResolved behavior', async () => { const identifier = `e2e-reactions-${Date.now()}`; const createRes = await session.testAgent.post('/v1/agents').send({ @@ -109,31 +109,24 @@ describe('Agents API - /agents #novu-v2', () => { expect(createRes.status).to.equal(201); expect(createRes.body.data.behavior).to.equal(undefined); - const setReactionsRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ - behavior: { - reactions: { onMessageReceived: 'wave', onResolved: 'thumbs_up' }, - }, + const setRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ + behavior: { reactionOnResolved: 'thumbs_up' }, }); - expect(setReactionsRes.status).to.equal(200); - expect(setReactionsRes.body.data.behavior.reactions.onMessageReceived).to.equal('wave'); - expect(setReactionsRes.body.data.behavior.reactions.onResolved).to.equal('thumbs_up'); + expect(setRes.status).to.equal(200); + expect(setRes.body.data.behavior.reactionOnResolved).to.equal('thumbs_up'); const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`); expect(getRes.status).to.equal(200); - expect(getRes.body.data.behavior.reactions.onMessageReceived).to.equal('wave'); - expect(getRes.body.data.behavior.reactions.onResolved).to.equal('thumbs_up'); + expect(getRes.body.data.behavior.reactionOnResolved).to.equal('thumbs_up'); const disableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ - behavior: { - reactions: { onMessageReceived: null }, - }, + behavior: { reactionOnResolved: null }, }); expect(disableRes.status).to.equal(200); - expect(disableRes.body.data.behavior.reactions.onMessageReceived).to.equal(null); - expect(disableRes.body.data.behavior.reactions.onResolved).to.equal('thumbs_up'); + expect(disableRes.body.data.behavior.reactionOnResolved).to.equal(null); await session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`); }); diff --git a/apps/api/src/app/agents/services/agent-config-resolver.service.ts b/apps/api/src/app/agents/services/agent-config-resolver.service.ts index ecb6f219e04..8e9326cd999 100644 --- a/apps/api/src/app/agents/services/agent-config-resolver.service.ts +++ b/apps/api/src/app/agents/services/agent-config-resolver.service.ts @@ -33,21 +33,15 @@ export interface ResolvedAgentConfig { agentIdentifier: string; integrationIdentifier: string; integrationId: string; - thinkingIndicatorEnabled: boolean; - reactionOnMessageReceived: WellKnownEmoji | null; + acknowledgeOnReceived: boolean; reactionOnResolved: WellKnownEmoji | null; bridgeUrl?: string; devBridgeUrl?: string; devBridgeActive?: boolean; } -const DEFAULT_REACTION_ON_MESSAGE: WellKnownEmoji = 'eyes'; const DEFAULT_REACTION_ON_RESOLVED: WellKnownEmoji = 'check'; -function resolveThinkingIndicator(agent: { behavior?: { thinkingIndicatorEnabled?: boolean } }): boolean { - return agent.behavior?.thinkingIndicatorEnabled !== false; -} - async function resolveReaction( value: string | null | undefined, defaultEmoji: WellKnownEmoji, @@ -156,14 +150,9 @@ export class AgentConfigResolver { agentIdentifier: agent.identifier, integrationIdentifier, integrationId: integration._id, - thinkingIndicatorEnabled: resolveThinkingIndicator(agent), - reactionOnMessageReceived: await resolveReaction( - agent.behavior?.reactions?.onMessageReceived, - DEFAULT_REACTION_ON_MESSAGE, - this.logger - ), + acknowledgeOnReceived: agent.behavior?.acknowledgeOnReceived !== false, reactionOnResolved: await resolveReaction( - agent.behavior?.reactions?.onResolved, + agent.behavior?.reactionOnResolved, DEFAULT_REACTION_ON_RESOLVED, this.logger ), diff --git a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts index d97b9e2b49d..d7913325eca 100644 --- a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts +++ b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts @@ -8,7 +8,7 @@ import { } from '@novu/dal'; import type { EmojiValue, Message, Thread } from 'chat'; import { AgentEventEnum } from '../dtos/agent-event.enum'; -import { PLATFORMS_WITHOUT_TYPING_INDICATOR } from '../dtos/agent-platform.enum'; +import { PLATFORMS_WITH_TYPING_INDICATOR } from '../dtos/agent-platform.enum'; import { HandleAgentReplyCommand } from '../usecases/handle-agent-reply/handle-agent-reply.command'; import { HandleAgentReply } from '../usecases/handle-agent-reply/handle-agent-reply.usecase'; import { ResolvedAgentConfig } from './agent-config-resolver.service'; @@ -17,6 +17,8 @@ import { AgentSubscriberResolver } from './agent-subscriber-resolver.service'; import type { AgentAction } from '@novu/framework'; import { BridgeExecutorService, type BridgeReaction, NoBridgeUrlError } from './bridge-executor.service'; +const ACKNOWLEDGE_FALLBACK_EMOJI = 'eyes' as const; + const ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN = `*You're connected to Novu* Your bot is linked successfully. Go back to the *Novu dashboard* to complete onboarding.`; @@ -116,23 +118,31 @@ export class AgentInboundHandler { const channel = conversation.channels?.[0]; const isFirstMessage = !channel?.firstPlatformMessageId; - if (isFirstMessage && config.reactionOnMessageReceived && message.id) { - thread - .createSentMessageFromMessage(message) - .addReaction(config.reactionOnMessageReceived) - .catch((err) => { - this.logger.warn(err, `[agent:${agentId}] Failed to add ack reaction to first message`); - }); - - this.conversationRepository - .setFirstPlatformMessageId(config.environmentId, config.organizationId, conversation._id, thread.id, message.id) - .catch((err) => { - this.logger.warn(err, `[agent:${agentId}] Failed to store firstPlatformMessageId`); - }); - } + if (config.acknowledgeOnReceived) { + const supportsTyping = PLATFORMS_WITH_TYPING_INDICATOR.has(config.platform); - if (config.thinkingIndicatorEnabled && !PLATFORMS_WITHOUT_TYPING_INDICATOR.has(config.platform)) { - await thread.startTyping('Thinking...'); + if (supportsTyping) { + await thread.startTyping('Thinking...'); + } else if (isFirstMessage && message.id) { + thread + .createSentMessageFromMessage(message) + .addReaction(ACKNOWLEDGE_FALLBACK_EMOJI) + .catch((err) => { + this.logger.warn(err, `[agent:${agentId}] Failed to add ack reaction to first message`); + }); + + this.conversationRepository + .setFirstPlatformMessageId( + config.environmentId, + config.organizationId, + conversation._id, + thread.id, + message.id + ) + .catch((err) => { + this.logger.warn(err, `[agent:${agentId}] Failed to store firstPlatformMessageId`); + }); + } } const serializedThread = thread.toJSON() as unknown as Record; diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index 3e5552c5838..312e680018a 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -36,7 +36,7 @@ const INSTANCE_TTL_MS = 1000 * 60 * 30; * resolved config. Event handlers registered via registerEventHandlers() close * over this box instead of the config value, so updates to fields that the * bridge executor and inbound handler read at event time (bridgeUrl, - * devBridgeUrl, devBridgeActive, thinkingIndicatorEnabled, reactions) take + * devBridgeUrl, devBridgeActive, acknowledgeOnReceived, reactionOnResolved) take * effect on the next inbound event without rebuilding the Chat instance. * * adapterFingerprint captures fields that are baked into the platform adapter diff --git a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts index 1cb610a5751..a73325864df 100644 --- a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts +++ b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts @@ -346,7 +346,7 @@ export class HandleAgentReply { channel: ConversationChannel ): Promise { const firstMessageId = channel.firstPlatformMessageId; - if (!firstMessageId || !config.reactionOnMessageReceived) return; + if (!firstMessageId || !config.acknowledgeOnReceived) return; await this.chatSdkService.removeReaction( conversation._agentId, @@ -354,7 +354,7 @@ export class HandleAgentReply { channel.platform, channel.platformThreadId, firstMessageId, - config.reactionOnMessageReceived + 'eyes' ); } diff --git a/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts b/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts index fdb69f3cfdb..6b952f03010 100644 --- a/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts +++ b/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts @@ -14,9 +14,8 @@ export class UpdateAgent { async execute(command: UpdateAgentCommand): Promise { const hasBehaviorFields = - command.behavior?.thinkingIndicatorEnabled !== undefined || - command.behavior?.reactions?.onMessageReceived !== undefined || - command.behavior?.reactions?.onResolved !== undefined; + command.behavior?.acknowledgeOnReceived !== undefined || + command.behavior?.reactionOnResolved !== undefined; const hasGeneralFields = command.name !== undefined || @@ -64,17 +63,11 @@ export class UpdateAgent { } if (hasBehaviorFields) { - if (command.behavior!.thinkingIndicatorEnabled !== undefined) { - $set['behavior.thinkingIndicatorEnabled'] = command.behavior!.thinkingIndicatorEnabled; + if (command.behavior!.acknowledgeOnReceived !== undefined) { + $set['behavior.acknowledgeOnReceived'] = command.behavior!.acknowledgeOnReceived; } - - if (command.behavior!.reactions !== undefined) { - if (command.behavior!.reactions.onMessageReceived !== undefined) { - $set['behavior.reactions.onMessageReceived'] = command.behavior!.reactions.onMessageReceived; - } - if (command.behavior!.reactions.onResolved !== undefined) { - $set['behavior.reactions.onResolved'] = command.behavior!.reactions.onResolved; - } + if (command.behavior!.reactionOnResolved !== undefined) { + $set['behavior.reactionOnResolved'] = command.behavior!.reactionOnResolved; } } diff --git a/apps/dashboard/src/api/agents.ts b/apps/dashboard/src/api/agents.ts index 6b716d364d1..957a6ce31c9 100644 --- a/apps/dashboard/src/api/agents.ts +++ b/apps/dashboard/src/api/agents.ts @@ -36,12 +36,18 @@ export type AgentIntegrationSummary = { active: boolean; }; +export type AgentBehavior = { + acknowledgeOnReceived?: boolean; + reactionOnResolved?: string | null; +}; + export type AgentResponse = { _id: string; name: string; identifier: string; description?: string; active: boolean; + behavior?: AgentBehavior; bridgeUrl?: string; devBridgeUrl?: string; devBridgeActive?: boolean; @@ -71,6 +77,7 @@ export type UpdateAgentBody = { name?: string; description?: string; active?: boolean; + behavior?: AgentBehavior; bridgeUrl?: string; devBridgeUrl?: string; devBridgeActive?: boolean; diff --git a/apps/dashboard/src/components/agents/agent-behavior-section.tsx b/apps/dashboard/src/components/agents/agent-behavior-section.tsx index ce1a85a7571..dc00d32ec2c 100644 --- a/apps/dashboard/src/components/agents/agent-behavior-section.tsx +++ b/apps/dashboard/src/components/agents/agent-behavior-section.tsx @@ -1,12 +1,20 @@ -import { useQuery } from '@tanstack/react-query'; -import { useMemo } from 'react'; -import { RiExpandUpDownLine } from 'react-icons/ri'; -import { type AgentEmojiEntry, getAgentEmojiQueryKey, listAgentEmoji } from '@/api/agents'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; +import { RiCloseCircleLine, RiExpandUpDownLine } from 'react-icons/ri'; +import { + type AgentEmojiEntry, + type AgentResponse, + getAgentDetailQueryKey, + getAgentEmojiQueryKey, + listAgentEmoji, + updateAgent, +} from '@/api/agents'; import { HelpTooltipIndicator } from '@/components/primitives/help-tooltip-indicator'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover'; +import { showErrorToast } from '@/components/primitives/sonner-helpers'; import { Switch } from '@/components/primitives/switch'; -import { useEnvironment } from '@/context/environment/hooks'; +import { requireEnvironment, useEnvironment } from '@/context/environment/hooks'; -const DEFAULT_REACTION_ON_MESSAGE = 'eyes'; const DEFAULT_REACTION_ON_RESOLVED = 'check'; function useAgentEmoji() { @@ -37,82 +45,140 @@ function SectionHeader({ children }: { children: React.ReactNode }) { ); } -function ToggleRow({ label, children }: { label: string; children: React.ReactNode }) { +function ToggleRow({ label, tooltip, children }: { label: string; tooltip: string; children: React.ReactNode }) { return (
{label} - +
{children}
); } -function EmojiPickerButton({ emoji }: { emoji: string }) { +type ResolvedEmojiPickerProps = { + currentEmoji: string | null; + emojiList: AgentEmojiEntry[]; + unicodeMap: Map; + disabled?: boolean; + onSelect: (emojiName: string | null) => void; +}; + +function ResolvedEmojiPicker({ currentEmoji, emojiList, unicodeMap, disabled, onSelect }: ResolvedEmojiPickerProps) { + const [open, setOpen] = useState(false); + const displayUnicode = currentEmoji ? unicodeMap.get(currentEmoji) : undefined; + return ( - + + + + + +
+ + {emojiList.map((entry) => ( + + ))} +
+
+
); } -export function AgentBehaviorSection() { - const { unicodeMap } = useAgentEmoji(); - const messageEmoji = unicodeMap.get(DEFAULT_REACTION_ON_MESSAGE) ?? ''; - const resolvedEmoji = unicodeMap.get(DEFAULT_REACTION_ON_RESOLVED) ?? ''; +type AgentBehaviorSectionProps = { + agent: AgentResponse; +}; + +export function AgentBehaviorSection({ agent }: AgentBehaviorSectionProps) { + const queryClient = useQueryClient(); + const { currentEnvironment } = useEnvironment(); + const { emojiList, unicodeMap } = useAgentEmoji(); + + const acknowledgeOnReceived = agent.behavior?.acknowledgeOnReceived !== false; + const reactionOnResolved = agent.behavior?.reactionOnResolved === undefined + ? DEFAULT_REACTION_ON_RESOLVED + : agent.behavior.reactionOnResolved; + + const { mutate, isPending } = useMutation({ + mutationFn: (body: { acknowledgeOnReceived?: boolean; reactionOnResolved?: string | null }) => + updateAgent( + requireEnvironment(currentEnvironment, 'No environment selected'), + agent.identifier, + { behavior: body } + ), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: getAgentDetailQueryKey(currentEnvironment?._id, agent.identifier), + }); + }, + onError: (err: Error) => { + showErrorToast(err.message, 'Failed to update behavior'); + }, + }); return (
Agent behavior
- {/* - Interrupt mode and response timeout are not supported yet. - -
- - - - - -
- 30 - seconds -
-
-
- */} -
- - - - - - + + mutate({ acknowledgeOnReceived: checked })} + /> - - + + mutate({ reactionOnResolved: emojiName })} + />
diff --git a/apps/dashboard/src/components/agents/agent-connected-overview.tsx b/apps/dashboard/src/components/agents/agent-connected-overview.tsx index e2f2b1b8e84..b6d759899ca 100644 --- a/apps/dashboard/src/components/agents/agent-connected-overview.tsx +++ b/apps/dashboard/src/components/agents/agent-connected-overview.tsx @@ -10,7 +10,7 @@ type AgentConnectedOverviewProps = { export function AgentConnectedOverview({ agent }: AgentConnectedOverviewProps) { return (
- +
diff --git a/apps/dashboard/src/components/agents/agent-setup-guide.tsx b/apps/dashboard/src/components/agents/agent-setup-guide.tsx index fed501e61ec..69d1817ae30 100644 --- a/apps/dashboard/src/components/agents/agent-setup-guide.tsx +++ b/apps/dashboard/src/components/agents/agent-setup-guide.tsx @@ -1,5 +1,5 @@ import { ChatProviderIdEnum } from '@novu/shared'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo, useState } from 'react'; import { RiExpandUpDownLine } from 'react-icons/ri'; import { type AgentResponse, getAgentIntegrationsQueryKey, listAgentIntegrations } from '@/api/agents'; @@ -34,9 +34,9 @@ function resolveProviderSetupGuide(providerId: string) { export function AgentSetupGuide({ agent }: AgentSetupGuideProps) { const [isExpanded, setIsExpanded] = useState(true); const [selectedIntegrationId, setSelectedIntegrationId] = useState(undefined); - const [isProviderComplete, setIsProviderComplete] = useState(false); const { currentEnvironment } = useEnvironment(); const { integrations } = useFetchIntegrations(); + const queryClient = useQueryClient(); const agentIntegrationsQuery = useQuery({ queryKey: getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier), @@ -49,14 +49,15 @@ export function AgentSetupGuide({ agent }: AgentSetupGuideProps) { enabled: Boolean(currentEnvironment && agent.identifier), }); + // Only reveal "2/2 Connect your code" once an integration link has actually been + // marked connected by the backend (connectedAt set). The OAuth success handler + // below refetches the list so this updates promptly after the user finishes the flow. const hasConnectedIntegration = useMemo(() => { - if (isProviderComplete) return true; - const links = agentIntegrationsQuery.data?.data; if (!links?.length) return false; return links.some((link) => Boolean(link.connectedAt)); - }, [isProviderComplete, agentIntegrationsQuery.data?.data]); + }, [agentIntegrationsQuery.data?.data]); const defaultFromAgent = agent.integrations?.[0]; @@ -82,8 +83,10 @@ export function AgentSetupGuide({ agent }: AgentSetupGuideProps) { const ProviderGuide = selectedProviderId ? resolveProviderSetupGuide(selectedProviderId) : null; const handleProviderStepsCompleted = useCallback(() => { - setIsProviderComplete(true); - }, []); + queryClient.invalidateQueries({ + queryKey: getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier), + }); + }, [queryClient, currentEnvironment?._id, agent.identifier]); return (
diff --git a/apps/dashboard/src/components/agents/slack-setup-guide.tsx b/apps/dashboard/src/components/agents/slack-setup-guide.tsx index ed31bd8a73b..cf9f73902cb 100644 --- a/apps/dashboard/src/components/agents/slack-setup-guide.tsx +++ b/apps/dashboard/src/components/agents/slack-setup-guide.tsx @@ -266,6 +266,7 @@ export function SlackSetupGuide({ connectionMode="subscriber" connectLabel={`Install ${agent.name} ↗`} connectedLabel="Connected to Slack" + onConnectSuccess={handleSlackWorkspaceConnected} /> ) : null diff --git a/apps/dashboard/src/components/agents/whatsapp-setup-guide.tsx b/apps/dashboard/src/components/agents/whatsapp-setup-guide.tsx index ad623690122..49971819433 100644 --- a/apps/dashboard/src/components/agents/whatsapp-setup-guide.tsx +++ b/apps/dashboard/src/components/agents/whatsapp-setup-guide.tsx @@ -85,14 +85,14 @@ export function WhatsAppSetupGuide({ const firstIncompleteStep = useMemo(() => { if (isConnected) { - return base + 3; + return base + 5; } if (!isCredentialsSaved) { return base; } - return base + 2; + return base + 3; }, [base, isCredentialsSaved, isConnected]); const stepsColumn = ( @@ -100,44 +100,78 @@ export function WhatsAppSetupGuide({ - {'Go to the '} + {'Open the '} - Meta Developer Portal + Meta App Dashboard - {', click '} + {' and click '} Create App - {' and select the '} - Business - { - ' type. Add the WhatsApp product to your app, then copy these values from your app dashboard into the credentials form:' + {'. Select the '} + Connect with customers through WhatsApp + {' use case, then pick or create a business portfolio.'} + + } + rightContent={ + } + > + Meta App Dashboard + + } + /> + + + {'After creating the app you land on the Quickstart page. Go to '} + WhatsApp > API Setup + {' and collect these four values:'} } extraContent={ -
    +
    1. - Access Token — WhatsApp > API Setup + Access Token — click "Generate access token" on the + API Setup page
    2. - Phone Number ID — WhatsApp > API Setup + Phone Number ID — shown under your selected phone number on the + API Setup page
    3. - App Secret — App Settings > Basic + App Secret — found under App Settings > Basic
    4. - Verify Token — a secret string of your choice (you'll reuse it - in the next step) + Verify Token — choose any secret string (you will reuse it when + configuring the webhook)
    5. -
+ } + /> + + {'In your Meta app, go to '} WhatsApp > Configuration - { - '. Paste the webhook URL below as the Callback URL, and set the Verify Token to the same secret string you entered in the previous step.' - } + {'. Set the '} + Callback URL + {' to the webhook URL below, and enter the same '} + Verify Token + {' you chose earlier.'} } extraContent={ } rightContent={} /> } /> @@ -210,8 +246,8 @@ export function WhatsAppSetupGuide({ agentIdentifier={agent.identifier} watchedIntegrationId={integrationId} onConnected={handleConnected} - connectedMessage="Your WhatsApp Business account is connected. This agent is ready to receive messages." - listeningMessage="Send a WhatsApp message to your business number to verify configuration." + connectedMessage="WhatsApp is connected — your agent is ready to receive messages." + listeningMessage="Waiting for a message on your business number to confirm the webhook is working…" /> ); diff --git a/libs/dal/src/repositories/agent/agent.entity.ts b/libs/dal/src/repositories/agent/agent.entity.ts index 18c59975843..38dac165dbf 100644 --- a/libs/dal/src/repositories/agent/agent.entity.ts +++ b/libs/dal/src/repositories/agent/agent.entity.ts @@ -2,16 +2,9 @@ import type { ChangePropsValueType } from '../../types/helpers'; import type { EnvironmentId } from '../environment'; import type { OrganizationId } from '../organization'; -export interface AgentReactionSettings { - /** Emoji name for acknowledging incoming messages (null = disabled, undefined = default "eyes") */ - onMessageReceived?: string | null; - /** Emoji name for resolved conversations (null = disabled, undefined = default "check") */ - onResolved?: string | null; -} - export interface AgentBehavior { - thinkingIndicatorEnabled?: boolean; - reactions?: AgentReactionSettings; + acknowledgeOnReceived?: boolean; + reactionOnResolved?: string | null; } export class AgentEntity { diff --git a/libs/dal/src/repositories/agent/agent.schema.ts b/libs/dal/src/repositories/agent/agent.schema.ts index ae6928495ba..a5a8547a7ce 100644 --- a/libs/dal/src/repositories/agent/agent.schema.ts +++ b/libs/dal/src/repositories/agent/agent.schema.ts @@ -19,11 +19,8 @@ const agentSchema = new Schema( default: true, }, behavior: { - thinkingIndicatorEnabled: Schema.Types.Boolean, - reactions: { - onMessageReceived: Schema.Types.Mixed, - onResolved: Schema.Types.Mixed, - }, + acknowledgeOnReceived: Schema.Types.Boolean, + reactionOnResolved: Schema.Types.String, }, bridgeUrl: Schema.Types.String, devBridgeUrl: Schema.Types.String,