Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
92 changes: 92 additions & 0 deletions apps/dashboard/src/components/agents/agent-behavior-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { RiExpandUpDownLine } from 'react-icons/ri';
import { HelpTooltipIndicator } from '@/components/primitives/help-tooltip-indicator';
import { Switch } from '@/components/primitives/switch';

function SectionHeader({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center px-2 py-1.5">
<span className="text-text-soft font-code text-[11px] font-medium uppercase leading-4 tracking-wider">
{children}
</span>
</div>
);
}

function ToggleRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-2 py-1">
<div className="flex flex-1 items-center gap-1">
<span className="text-text-sub text-label-sm font-medium">{label}</span>
<HelpTooltipIndicator text={label} size="5" />
</div>
{children}
</div>
);
}

function EmojiPickerButton({ emoji }: { emoji: string }) {
return (
<button
type="button"
className="border-stroke-soft bg-bg-white flex shrink-0 items-center gap-1 rounded-md border px-1.5 py-[3px] shadow-xs"
>
<span className="text-label-sm leading-5">{emoji}</span>
<RiExpandUpDownLine className="text-text-soft size-3" />
</button>
);
}

export function AgentBehaviorSection() {
return (
<div className="bg-bg-weak flex flex-col rounded-[10px] p-1">
<SectionHeader>Agent behavior</SectionHeader>
<div className="bg-bg-white flex flex-col overflow-hidden rounded-md shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]">
{/*
Interrupt mode and response timeout are not supported yet.

<div className="border-stroke-weak flex flex-col gap-3 border-b p-3">
<SettingRow
label="Interrupt mode"
description="What happens when a new message arrives while the agent is still responding."
>
<Select defaultValue="drop_respond">
<SelectTrigger
className="border-stroke-soft bg-bg-white text-text-strong text-label-sm h-8 w-full rounded-md shadow-xs"
rightIcon={<RiExpandUpDownLine className="text-text-soft size-3" />}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="drop_respond">Drop and respond new message</SelectItem>
<SelectItem value="queue">Queue and respond in order</SelectItem>
<SelectItem value="ignore">Ignore new message</SelectItem>
</SelectContent>
</Select>
</SettingRow>

<SettingRow label="Response timeout">
<div className="border-stroke-soft bg-bg-white flex h-8 w-full items-center justify-between overflow-hidden rounded-md border px-2 shadow-xs">
<span className="text-text-strong text-label-sm font-medium">30</span>
<span className="text-text-soft text-label-sm font-medium">seconds</span>
</div>
</SettingRow>
</div>
*/}

<div className="flex flex-col gap-2 p-3">
<ToggleRow label={'Show "Typing..." indicator when available'}>
<Switch defaultChecked />
</ToggleRow>

<ToggleRow label="React to incoming messages so users know the agent received them">
<EmojiPickerButton emoji="👀" />
</ToggleRow>

<ToggleRow label="React to the final message when a conversation is resolved">
<EmojiPickerButton emoji="✅" />
</ToggleRow>
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions apps/dashboard/src/components/agents/agent-connected-overview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AgentResponse } from '@/api/agents';
import { AgentBehaviorSection } from './agent-behavior-section';
import { ConnectedProvidersSection } from './connected-providers-section';
import { RecentConversationsSection } from './recent-conversations-section';

type AgentConnectedOverviewProps = {
agent: AgentResponse;
};

export function AgentConnectedOverview({ agent }: AgentConnectedOverviewProps) {
return (
<div className="flex min-w-0 flex-1 flex-col gap-4">
<AgentBehaviorSection />
<ConnectedProvidersSection agent={agent} />
<RecentConversationsSection agent={agent} />
</div>
);
}
5 changes: 4 additions & 1 deletion apps/dashboard/src/components/agents/agent-overview-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentResponse } from '@/api/agents';
import { AgentConnectedOverview } from '@/components/agents/agent-connected-overview';
import { AgentSetupGuide } from '@/components/agents/agent-setup-guide';
import { AgentSidebarWidget } from '@/components/agents/agent-sidebar-widget';

Expand All @@ -7,10 +8,12 @@ type AgentOverviewTabProps = {
};

export function AgentOverviewTab({ agent }: AgentOverviewTabProps) {
const isBridgeConnected = Boolean(agent.bridgeUrl || (agent.devBridgeActive && agent.devBridgeUrl));

return (
<div className="flex gap-6 px-6 pt-4">
<AgentSidebarWidget agent={agent} />
<AgentSetupGuide agent={agent} />
{isBridgeConnected ? <AgentConnectedOverview agent={agent} /> : <AgentSetupGuide agent={agent} />}
</div>
);
}
121 changes: 82 additions & 39 deletions apps/dashboard/src/components/agents/agent-setup-guide.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ChatProviderIdEnum } from '@novu/shared';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { RiExpandUpDownLine } from 'react-icons/ri';
import { type AgentResponse } from '@/api/agents';
import { type AgentResponse, getAgentIntegrationsQueryKey, listAgentIntegrations } from '@/api/agents';
import { requireEnvironment, useEnvironment } from '@/context/environment/hooks';
import { useFetchIntegrations } from '@/hooks/use-fetch-integrations';
import { cn } from '@/utils/ui';
import { AgentCodeSetupSection } from './agent-code-setup-section';
Expand All @@ -23,12 +25,45 @@ function resolveProviderSetupGuide(providerId: string) {
}
}

function AgentSetupGuideComingSoon() {

return (
<div className="border-stroke-soft bg-bg-weak/30 flex flex-col items-center justify-center rounded-md border border-dashed px-6 py-12 text-center">
<p className="text-text-strong text-label-sm font-medium">Coming soon</p>
<p className="text-text-soft text-label-xs mt-2 max-w-sm leading-5">
In-dashboard setup steps will return here as we expand agent tooling.
</p>
</div>
);
}

export function AgentSetupGuide({ agent }: AgentSetupGuideProps) {
const [isExpanded, setIsExpanded] = useState(true);
const [selectedIntegrationId, setSelectedIntegrationId] = useState<string | undefined>(undefined);
const [isProviderComplete, setIsProviderComplete] = useState(false);
const { currentEnvironment } = useEnvironment();
const { integrations } = useFetchIntegrations();

const agentIntegrationsQuery = useQuery({
queryKey: getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier),
queryFn: () =>
listAgentIntegrations({
environment: requireEnvironment(currentEnvironment, 'No environment selected'),
agentIdentifier: agent.identifier,
limit: 100,
}),
enabled: Boolean(currentEnvironment && agent.identifier),
});

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]);

const slackFromAgent = agent.integrations?.find((i) => i.providerId === ChatProviderIdEnum.Slack);

const effectiveIntegrationId = selectedIntegrationId ?? slackFromAgent?.integrationId;
Expand Down Expand Up @@ -56,6 +91,8 @@ export function AgentSetupGuide({ agent }: AgentSetupGuideProps) {
setIsProviderComplete(true);
}, []);

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

return (
<div className="bg-bg-weak flex min-w-0 flex-1 flex-col rounded-[10px] p-1">
<button
Expand All @@ -69,46 +106,52 @@ export function AgentSetupGuide({ agent }: AgentSetupGuideProps) {

{isExpanded && (
<div className="bg-bg-white flex flex-col gap-0 overflow-hidden rounded-md p-3 shadow-[0px_0px_0px_1px_rgba(25,28,33,0.04),0px_1px_2px_0px_rgba(25,28,33,0.06),0px_0px_2px_0px_rgba(0,0,0,0.08)]">
<div className="relative flex flex-col gap-10 py-6 pb-3 pl-8 pr-6">
<div
className="absolute bottom-0 left-[22px] top-0 w-px"
style={{
background: 'linear-gradient(to bottom, transparent 0%, #E1E4EA 10%, #E1E4EA 90%, transparent 100%)',
}}
/>

<SetupStep
index={1}
status={deriveStepStatus(1, firstIncompleteStepForProviderRow)}
sectionLabel="1/2 SETUP PROVIDER"
title="Choose where your agent listens and communicates"
description="Start with one provider your agent can receive and respond on and you can always add more providers as you need."
rightContent={
<ProviderDropdown
agentIdentifier={agent.identifier}
selectedIntegrationId={selectedIntegrationId ?? slackFromAgent?.integrationId}
linkedIntegrationIds={linkedIntegrationIds}
onSelect={(_providerId, integration) => {
if (integration?._id) {
setSelectedIntegrationId(integration._id);
}
}}
/>
}
/>

{ProviderGuide && effectiveIntegrationId ? (
<ProviderGuide
agent={agent}
integrationId={effectiveIntegrationId}
stepOffset={2}
embedded={false}
onStepsCompleted={handleProviderStepsCompleted}
{isBridgeConnected ? (
<AgentSetupGuideComingSoon />
) : (
<div className="relative flex flex-col gap-10 py-6 pb-3 pl-8 pr-6">
<div
className="absolute bottom-0 left-[22px] top-0 w-px"
style={{
background: 'linear-gradient(to bottom, transparent 0%, #E1E4EA 10%, #E1E4EA 90%, transparent 100%)',
}}
/>
) : null}

<AgentCodeSetupSection agent={agent} stepOffset={5} isProviderComplete={isProviderComplete} />
</div>
<SetupStep
index={1}
status={deriveStepStatus(1, firstIncompleteStepForProviderRow)}
sectionLabel="1/2 SETUP PROVIDER"
title="Choose where your agent listens and communicates"
description="Start with one provider your agent can receive and respond on and you can always add more providers as you need."
rightContent={
<ProviderDropdown
agentIdentifier={agent.identifier}
selectedIntegrationId={selectedIntegrationId ?? slackFromAgent?.integrationId}
linkedIntegrationIds={linkedIntegrationIds}
onSelect={(_providerId, integration) => {
if (integration?._id) {
setSelectedIntegrationId(integration._id);
}
}}
/>
}
/>

{ProviderGuide && effectiveIntegrationId ? (
<ProviderGuide
agent={agent}
integrationId={effectiveIntegrationId}
stepOffset={2}
embedded={false}
onStepsCompleted={handleProviderStepsCompleted}
/>
) : null}

{hasConnectedIntegration && (
<AgentCodeSetupSection agent={agent} stepOffset={5} isProviderComplete={hasConnectedIntegration} />
)}
</div>
)}
</div>
)}
</div>
Expand Down
Loading
Loading