Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions apps/dashboard/src/components/agents/agent-code-setup-section.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChatProviderIdEnum } from '@novu/shared';
import { CheckCircle2, Loader } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { RiCheckLine, RiFileCopyLine } from 'react-icons/ri';
Expand Down Expand Up @@ -107,12 +108,25 @@ function TerminalBlock({ displayCommand, copyCommand }: { displayCommand: string
);
}

function getProviderCallToAction(providerId: string | undefined): string {
switch (providerId) {
case ChatProviderIdEnum.Slack:
return 'Head back to Slack and mention your bot again — this time your agent server will handle the message.';
case ChatProviderIdEnum.WhatsAppBusiness:
return 'Send a message to your WhatsApp number again — this time your agent server will handle it.';
default:
return 'Send a message to your bot from the connected provider again — this time your agent server will handle it.';
}
}

function BridgeConnectionStatus({
agent,
agentIdentifier,
providerId,
}: {
agent: AgentResponse;
agentIdentifier: string;
providerId: string | undefined;
}) {
const { currentEnvironment } = useEnvironment();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -165,11 +179,12 @@ function BridgeConnectionStatus({
<div className="flex flex-col gap-2 py-4 pl-8">
<div className="flex items-center gap-1">
<CheckCircle2 className="text-success-base size-3.5 shrink-0" />
<span className="text-text-strong text-label-sm font-medium">Bridge connected</span>
<span className="text-text-strong text-label-sm font-medium">Bridge connected — try your agent</span>
</div>
<p className="text-text-soft text-label-xs font-medium leading-4">{getProviderCallToAction(providerId)}</p>
<p className="text-text-soft text-label-xs font-medium leading-4">
Your agent is receiving events. Edit{' '}
<code className="font-code text-[12px] tracking-[-0.24px]">app/novu/agents/</code> to customize the handler.
Edit <code className="font-code text-[12px] tracking-[-0.24px]">app/novu/agents/</code> to customize how your
agent responds.
</p>
<ExternalLink href="https://docs.novu.co/agents/overview" variant="documentation">
Agent documentation
Expand All @@ -196,10 +211,10 @@ function BridgeConnectionStatus({
type AgentCodeSetupSectionProps = {
agent: AgentResponse;
stepOffset: number;
isProviderComplete: boolean;
providerId?: string;
};

export function AgentCodeSetupSection({ agent, stepOffset, isProviderComplete }: AgentCodeSetupSectionProps) {
export function AgentCodeSetupSection({ agent, stepOffset, providerId }: AgentCodeSetupSectionProps) {
const apiKeysQuery = useFetchApiKeys();
const secretKey = apiKeysQuery.data?.data?.[0]?.key;

Expand All @@ -208,14 +223,9 @@ export function AgentCodeSetupSection({ agent, stepOffset, isProviderComplete }:

const isBridgeConnected = Boolean(agent.bridgeUrl || (agent.devBridgeActive && agent.devBridgeUrl));

let firstIncompleteStep: number;
if (isBridgeConnected) {
firstIncompleteStep = stepOffset + 2;
} else if (isProviderComplete) {
firstIncompleteStep = stepOffset;
} else {
firstIncompleteStep = stepOffset + 3;
}
// The caller only renders this section once a provider integration is
// connected, so the "2/2 Connect your code" steps start out active.
const firstIncompleteStep = isBridgeConnected ? stepOffset + 2 : stepOffset;

return (
<>
Expand Down Expand Up @@ -271,7 +281,7 @@ export function AgentCodeSetupSection({ agent, stepOffset, isProviderComplete }:
}
/>

<BridgeConnectionStatus agent={agent} agentIdentifier={agent.identifier} />
<BridgeConnectionStatus agent={agent} agentIdentifier={agent.identifier} providerId={providerId} />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function AgentConnectedOverview({ agent }: AgentConnectedOverviewProps) {
<div className="flex min-w-0 flex-1 flex-col gap-4">
<AgentBehaviorSection />
<ConnectedProvidersSection agent={agent} />
<RecentConversationsSection agent={agent} />
<RecentConversationsSection />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function SlackAgentIntegrationGuide({
onRequestRemoveIntegration,
isRemovingIntegration,
}: SlackAgentIntegrationGuideProps) {
const isConnected = Boolean(integrationLink?.connectedAt);

return (
<AgentIntegrationGuideLayout
Expand All @@ -37,29 +38,38 @@ export function SlackAgentIntegrationGuide({
isRemovingIntegration={isRemovingIntegration}
>
<AgentIntegrationGuideSection title="Overview">
<p>
Connect Slack so this agent can send and receive chat messages through your workspace. Ensure the integration
is configured and active in the integration store for this environment.
</p>
{isConnected ? (
<p>
This agent is connected to Slack. Tag the bot in any channel it has been invited to, or DM it directly, to
send and receive chat messages through your workspace.
</p>
) : (
<p>
Connect Slack so this agent can send and receive chat messages through your workspace. Ensure the
integration is configured and active in the integration store for this environment.
</p>
)}
</AgentIntegrationGuideSection>
<div className="flex flex-col gap-3">
<p className="text-text-strong text-label-sm font-medium">Steps</p>
<AgentIntegrationGuideStep
step={1}
title="Install the Slack app"
description="Complete OAuth in the integration store and grant the channels your agent should use."
/>
<AgentIntegrationGuideStep
step={2}
title="Verify credentials"
description="Confirm the integration shows as active for this environment before testing the agent."
/>
<AgentIntegrationGuideStep
step={3}
title="Test from the agent"
description="Send a test message from your application and confirm delivery in Slack."
/>
</div>
{!isConnected && (
<div className="flex flex-col gap-3">
<p className="text-text-strong text-label-sm font-medium">Steps</p>
<AgentIntegrationGuideStep
step={1}
title="Install the Slack app"
description="Complete OAuth in the integration store and grant the channels your agent should use."
/>
<AgentIntegrationGuideStep
step={2}
title="Verify credentials"
description="Confirm the integration shows as active for this environment before testing the agent."
/>
<AgentIntegrationGuideStep
step={3}
title="Test from the agent"
description="Send a test message from your application and confirm delivery in Slack."
/>
</div>
)}
</AgentIntegrationGuideLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function WhatsAppAgentIntegrationGuide({
onRequestRemoveIntegration,
isRemovingIntegration,
}: WhatsAppAgentIntegrationGuideProps) {
const isConnected = Boolean(integrationLink?.connectedAt);

return (
<AgentIntegrationGuideLayout
Expand All @@ -37,29 +38,38 @@ export function WhatsAppAgentIntegrationGuide({
isRemovingIntegration={isRemovingIntegration}
>
<AgentIntegrationGuideSection title="Overview">
<p>
This agent sends and receives messages through your WhatsApp Business phone number. If you need to update
credentials or reconfigure the webhook, follow the steps below.
</p>
{isConnected ? (
<p>
This agent is connected to WhatsApp Business. Send a message to your business phone number to start a
conversation — replies are routed through your agent server.
</p>
) : (
<p>
Connect WhatsApp Business so this agent can send and receive messages through your business phone number.
Follow the steps below to create a Meta app, configure the webhook, and verify the connection.
</p>
)}
</AgentIntegrationGuideSection>
<div className="flex flex-col gap-3">
<p className="text-text-strong text-label-sm font-medium">Steps</p>
<AgentIntegrationGuideStep
step={1}
title="Create a Meta app and get credentials"
description="Go to developers.facebook.com/apps, create a Business-type app, and add the WhatsApp product. Copy the Access Token and Phone Number ID from WhatsApp > API Setup, and the App Secret from App Settings > Basic. For production, generate a permanent System User Token instead of the temporary access token."
/>
<AgentIntegrationGuideStep
step={2}
title="Configure the webhook"
description="In your Meta app go to WhatsApp > Configuration. Set the Callback URL to the webhook URL shown above and the Verify Token to the same secret you entered in the credentials. Subscribe to the 'messages' webhook field so the agent receives inbound messages."
/>
<AgentIntegrationGuideStep
step={3}
title="Verify the connection"
description="Send a WhatsApp message to your business phone number and confirm the agent receives and responds."
/>
</div>
{!isConnected && (
<div className="flex flex-col gap-3">
<p className="text-text-strong text-label-sm font-medium">Steps</p>
<AgentIntegrationGuideStep
step={1}
title="Create a Meta app and get credentials"
description="Go to developers.facebook.com/apps, create a Business-type app, and add the WhatsApp product. Copy the Access Token and Phone Number ID from WhatsApp > API Setup, and the App Secret from App Settings > Basic. For production, generate a permanent System User Token instead of the temporary access token."
/>
<AgentIntegrationGuideStep
step={2}
title="Configure the webhook"
description="In your Meta app go to WhatsApp > Configuration. Set the Callback URL to the webhook URL shown above and the Verify Token to the same secret you entered in the credentials. Subscribe to the 'messages' webhook field so the agent receives inbound messages."
/>
<AgentIntegrationGuideStep
step={3}
title="Verify the connection"
description="Send a WhatsApp message to your business phone number and confirm the agent receives and responds."
/>
</div>
)}
</AgentIntegrationGuideLayout>
);
}
32 changes: 8 additions & 24 deletions apps/dashboard/src/components/agents/agent-integrations-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,6 @@ export function AgentIntegrationsTab({ agent, integrationIdentifier }: AgentInte

const integrationsStorePath = ROUTES.INTEGRATIONS;

const activityTabPath = `${buildRoute(ROUTES.AGENT_DETAILS_TAB, {
environmentSlug: currentEnvironment?.slug ?? '',
agentIdentifier: encodeURIComponent(agent.identifier),
agentTab: 'activity',
})}${location.search}`;

const navigateToGuide = (nextIntegrationIdentifier: string) => {
if (!currentEnvironment?.slug) {
return;
Expand Down Expand Up @@ -430,13 +424,16 @@ export function AgentIntegrationsTab({ agent, integrationIdentifier }: AgentInte
const int = link.integration;
const providerMeta = novuProviders.find((p) => p.id === int.providerId);
const isSelected = integrationIdentifier === int.identifier;
const showActionNeeded = !int.active;
const showActionNeeded = !link.connectedAt;

const statusLabel = showActionNeeded ? 'Action needed' : 'Active';

return (
<button
key={link._id}
type="button"
onClick={() => handleLinkedRowClick(link)}
aria-label={`${int.name} — ${statusLabel}`}
className={cn(
'bg-bg-white border-stroke-weak hover:border-stroke-soft flex w-full items-center justify-between gap-1.5 rounded-md border px-2 py-1.5 text-left transition-colors',
isSelected && 'border-stroke-soft'
Expand All @@ -452,20 +449,13 @@ export function AgentIntegrationsTab({ agent, integrationIdentifier }: AgentInte
{int.name}
</span>
</span>
<span className="flex shrink-0 items-center gap-1">
<span className="flex shrink-0 items-center gap-1" aria-hidden>
{showActionNeeded ? (
<RiErrorWarningFill
className="text-error-base size-3 shrink-0"
aria-label="Action needed"
/>
<RiErrorWarningFill className="text-warning-base size-3 shrink-0" />
) : (
<div
className="bg-success-base size-1.5 shrink-0 rounded-full"
role="img"
aria-label="Active"
/>
<div className="bg-success-base size-1.5 shrink-0 rounded-full" />
)}
<RiArrowRightSLine className="text-text-soft size-4 shrink-0" aria-hidden />
<RiArrowRightSLine className="text-text-soft size-4 shrink-0" />
</span>
</button>
);
Expand Down Expand Up @@ -507,12 +497,6 @@ export function AgentIntegrationsTab({ agent, integrationIdentifier }: AgentInte
<div className="border-stroke-soft border-t pt-3">
<p className="text-text-soft text-label-xs font-medium leading-4">Quick actions</p>
<div className="mt-3 flex flex-wrap gap-2">
<Link
to={activityTabPath}
className="border-stroke-soft text-text-strong hover:bg-bg-weak text-label-xs inline-flex h-7 items-center rounded-md border bg-transparent px-3 font-medium transition-colors"
>
View activity
</Link>
<Link
to={integrationsStorePath}
className="border-stroke-soft text-text-strong hover:bg-bg-weak text-label-xs inline-flex h-7 items-center rounded-md border bg-transparent px-3 font-medium transition-colors"
Expand Down
9 changes: 8 additions & 1 deletion apps/dashboard/src/components/agents/agent-overview-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import type { AgentResponse } from '@/api/agents';
import { AgentConnectedOverview } from '@/components/agents/agent-connected-overview';
import { AgentSetupGuide } from '@/components/agents/agent-setup-guide';
Expand All @@ -10,10 +11,16 @@ type AgentOverviewTabProps = {
export function AgentOverviewTab({ agent }: AgentOverviewTabProps) {
const isBridgeConnected = Boolean(agent.bridgeUrl || (agent.devBridgeActive && agent.devBridgeUrl));

// Snapshot connection state on mount so that users who are actively
// completing the quick-start stay on the setup guide (and see the
// completion step) even after the bridge connects mid-session. Users who
// arrive with a bridge already connected get the connected overview.
const [wasBridgeConnectedOnMount] = useState(isBridgeConnected);

return (
<div className="flex gap-6 px-6 pt-4">
<AgentSidebarWidget agent={agent} />
{isBridgeConnected ? <AgentConnectedOverview agent={agent} /> : <AgentSetupGuide agent={agent} />}
{wasBridgeConnectedOnMount ? <AgentConnectedOverview agent={agent} /> : <AgentSetupGuide agent={agent} />}
</div>
);
}
Loading
Loading