Conversation
Introduce a conversations feature: adds API client (conversations endpoints and DTOs) and types, plus React UI components (filters, table, table rows, detail/overview, timeline, status badge, empty state, content) to browse and inspect conversations. Adds hooks for data fetching and URL state (use-fetch-conversations, use-fetch-conversation-activities, use-conversation-url-state, use-conversation-url-state) with React Query integration, pagination and persisted page-size support. Updates query-keys and routes utilities and makes small adjustments in main.tsx and activity-feed.tsx to integrate the new feature. Supports filtering, date ranges, providers, pagination and a resizable detail panel with timeline.
Backend: fetch agent by identifier and pass agent name (senderName) into deliverMessage so outgoing ConversationActivity records include the agent's display name. Inject AgentRepository and adjust types/imports accordingly. Dashboard: add ParticipantSubscriberData and ParticipantAgentData types and include subscriber/agent fields on ConversationParticipantDto. Use subscriber first/last name and avatar for display, resolve agent name from participant.agent.name when present, replace vertical Separator with a simple divider element, and pass currentPageItemsCount to the pagination footer. Also update submodule reference (.source) to the new commit.
✅ Deploy preview added
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Hey there and thank you for opening this pull request! 👋 We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted. Your PR title is: Requirements:
Expected format: Details: PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughIntroduces a conversations feature to the dashboard with API endpoints, React components, and hooks for fetching, filtering, and displaying conversations; updates the agent reply usecase to validate agent identifiers before delivery; and integrates a conversations tab in the activity feed. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (3)
apps/dashboard/src/utils/query-keys.ts (1)
42-43: Consider co-locating conversation query keys with the conversations feature.Keeping these keys next to
apps/dashboard/src/api/conversations.ts(or feature hooks) improves discoverability and reduces cross-module coupling for future feature changes.As per coding guidelines
Co-locate query keys and fetcher functions in src/api/ or alongside the feature they belong to.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/utils/query-keys.ts` around lines 42 - 43, The query key constants fetchConversations and fetchConversation are defined in a central query-keys module but should be co-located with the conversations feature; move these constants out of query-keys.ts and into the conversations feature (e.g., alongside the fetcher functions or hooks in the conversations API module where functions like the conversations fetcher or useConversations hook live), then update any imports that reference fetchConversations/fetchConversation to import from the new module to improve discoverability and reduce cross-module coupling.apps/dashboard/src/components/conversations/conversations-filters.tsx (1)
24-27: Reuse sharedPROVIDER_OPTIONSinstead of duplicating provider mapping.This mapping duplicates
apps/dashboard/src/components/conversations/constants.tsand can drift over time. Prefer importing the shared constant.♻️ Suggested cleanup
-import { CONVERSATIONAL_PROVIDERS } from '@novu/shared'; +import { PROVIDER_OPTIONS } from './constants'; @@ -const PROVIDER_OPTIONS = CONVERSATIONAL_PROVIDERS.filter((p) => !p.comingSoon).map((p) => ({ - label: p.displayName, - value: p.providerId, -}));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/conversations/conversations-filters.tsx` around lines 24 - 27, The local PROVIDER_OPTIONS duplicates the provider mapping; remove this duplicate and import the shared PROVIDER_OPTIONS from the conversations/constants module instead (use the existing CONVERSATIONAL_PROVIDERS mapping there) so the component reuses the canonical source; if the shared constant doesn't already filter out comingSoon, either import the shared CONVERSATIONAL_PROVIDERS and apply .filter(p => !p.comingSoon).map(...) once in the shared module or import the shared PROVIDER_OPTIONS directly and replace all local usages in this file (e.g., where PROVIDER_OPTIONS and CONVERSATIONAL_PROVIDERS are referenced).apps/dashboard/src/components/conversations/conversation-timeline.tsx (1)
117-125: Add button semantics for expand/collapse state.Consider adding
type="button"andaria-expandedto make the toggle behavior clearer to assistive tech.Suggested fix
{isLong && ( <button + type="button" + aria-expanded={expanded} onClick={() => setExpanded(!expanded)} className="text-text-soft flex shrink-0 items-center gap-0.5" >As per coding guidelines
apps/dashboard/**: “Review with focus on UX, accessibility, and performance.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/conversations/conversation-timeline.tsx` around lines 117 - 125, The toggle button for expanding messages (the element using onClick={() => setExpanded(!expanded)} with RiExpandUpDownLine and the expanded state) lacks proper semantics; update the button element to include type="button" to prevent accidental form submission and add aria-expanded={expanded} so assistive tech can detect the current state (optionally add aria-controls referencing the ID of the collapsible content). Ensure you keep the existing onClick and text content while adding these attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts`:
- Around line 58-67: You are resolving agent info eagerly from
command.agentIdentifier via agentRepository.findOne and setting agentName, but
you must validate that this identity matches the conversation._agentId used for
delivery and avoid the lookup when the message is only a resolve/signals type;
update handle-agent-reply.usecase.ts to (1) lazily fetch agent data only when
required for writing activity/senderName (i.e., not for resolve/signals), (2)
when fetching via agentRepository.findOne, cross-check the returned agent._id
(or identifier) against conversation._agentId and throw or handle an
authorization/error if they mismatch, and (3) ensure agentName is populated from
the validated agent after the check so activity writes cannot record mismatched
agentId/senderName.
In `@apps/dashboard/src/components/conversations/conversation-detail.tsx`:
- Around line 29-47: The icon-only navigation and close buttons (the elements
invoking onNavigate('prev'), onNavigate('next') and onClose that render
RiArrowUpSLine, RiArrowDownSLine and RiCloseFill) must include accessible labels
and explicit button type to satisfy a11y: add type="button" and aria-label
attributes (e.g., aria-label="Previous conversation", "Next conversation",
"Close conversation" or use passed-in label props) to each respective button and
ensure the onClick handlers remain unchanged; update the JSX for those buttons
to include these attributes so screen readers receive meaningful names.
In `@apps/dashboard/src/components/conversations/conversation-status-badge.tsx`:
- Line 27: The current logic sets const config = STATUS_CONFIG[status] ||
STATUS_CONFIG.active which silently maps any unrecognized status to the
"active/OPEN" styling; change this so unknown statuses render a neutral/unknown
state instead: look up STATUS_CONFIG[status] into config and if it's undefined,
set config to a dedicated STATUS_CONFIG.unknown (or a constructed neutral
config) and ensure the component (ConversationStatusBadge) uses that config so
unknown statuses display an "UNKNOWN"/neutral label and styling rather than
defaulting to OPEN.
In `@apps/dashboard/src/components/conversations/conversation-table-row.tsx`:
- Around line 52-55: The TableRow currently handles only mouse clicks via
onClick={handleClick} and lacks keyboard and ARIA semantics; make it
keyboard-accessible by adding tabIndex={0} so it can receive focus, add
onKeyDown that listens for Enter and Space (triggering handleClick;
preventDefault for Space to avoid page scroll), and expose selection state with
aria-selected={isSelected} (and role="row" or appropriate role if not already
present) while keeping the existing className/cn usage; ensure you update the
JSX for the TableRow element (reference: TableRow, handleClick, isSelected, cn)
and keep behavior identical for mouse clicks.
In `@apps/dashboard/src/components/conversations/conversation-timeline.tsx`:
- Around line 112-115: The paragraph always applies the "truncate" class so even
when expanded is true the text stays clipped; update the JSX for the <p> that
renders displayContent (the element using className "text-label-xs min-w-0
flex-1 truncate font-medium text-[`#1a1a1a`]") to conditionally include "truncate"
only when the message is not expanded (e.g., className={`text-label-xs min-w-0
flex-1 font-medium text-[`#1a1a1a`] ${!expanded ? 'truncate' : ''}`}) or use a
helper like clsx; ensure this uses the existing expanded and isLong state so
expanded messages show full content.
In `@apps/dashboard/src/components/conversations/conversations-table.tsx`:
- Line 135: The TableCell in the ConversationsTable component currently sets
colSpan={7} which doesn't match the actual table header column count (2) and
breaks accessibility; update the footer TableCell's colSpan to colSpan={2} (in
the conversations-table component where TableCell is rendered) so the footer
spans the correct number of columns and restores proper table semantics for
assistive tech.
In `@apps/dashboard/src/hooks/use-conversation-url-state.ts`:
- Around line 86-88: The current logic in useConversationUrlState copies the
existing 'page' param (searchParams.get('page')) into newParams, which preserves
pagination across filter changes and can produce empty results; change the code
in the useConversationUrlState hook so filter updates do not carry over the old
page—either remove the block that sets newParams.set('page', ...) entirely or
explicitly reset newParams.set('page', '0') when filters change (referencing the
searchParams and newParams variables and the 'page' key) so pagination is
cleared on filter updates.
In `@apps/dashboard/src/pages/activity-feed.tsx`:
- Around line 25-27: The current check unconditionally returns 'conversations'
when location.pathname.includes('/activity/conversations'), which can set an
invalid active tab when isConversationalAgentsEnabled is false; update the logic
where the active tab is determined (the location.pathname check that returns
'conversations') to first guard with the feature flag
(isConversationalAgentsEnabled) and if the flag is false, return a safe fallback
tab (e.g., 'all' or the default tab) and trigger a redirect/navigation off
'/activity/conversations' to the fallback route so the UI renders a valid tab
instead of a broken view.
---
Nitpick comments:
In `@apps/dashboard/src/components/conversations/conversation-timeline.tsx`:
- Around line 117-125: The toggle button for expanding messages (the element
using onClick={() => setExpanded(!expanded)} with RiExpandUpDownLine and the
expanded state) lacks proper semantics; update the button element to include
type="button" to prevent accidental form submission and add
aria-expanded={expanded} so assistive tech can detect the current state
(optionally add aria-controls referencing the ID of the collapsible content).
Ensure you keep the existing onClick and text content while adding these
attributes.
In `@apps/dashboard/src/components/conversations/conversations-filters.tsx`:
- Around line 24-27: The local PROVIDER_OPTIONS duplicates the provider mapping;
remove this duplicate and import the shared PROVIDER_OPTIONS from the
conversations/constants module instead (use the existing
CONVERSATIONAL_PROVIDERS mapping there) so the component reuses the canonical
source; if the shared constant doesn't already filter out comingSoon, either
import the shared CONVERSATIONAL_PROVIDERS and apply .filter(p =>
!p.comingSoon).map(...) once in the shared module or import the shared
PROVIDER_OPTIONS directly and replace all local usages in this file (e.g., where
PROVIDER_OPTIONS and CONVERSATIONAL_PROVIDERS are referenced).
In `@apps/dashboard/src/utils/query-keys.ts`:
- Around line 42-43: The query key constants fetchConversations and
fetchConversation are defined in a central query-keys module but should be
co-located with the conversations feature; move these constants out of
query-keys.ts and into the conversations feature (e.g., alongside the fetcher
functions or hooks in the conversations API module where functions like the
conversations fetcher or useConversations hook live), then update any imports
that reference fetchConversations/fetchConversation to import from the new
module to improve discoverability and reduce cross-module coupling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6c5f6e6a-acb5-476b-8afa-f8e4d9541890
📒 Files selected for processing (21)
.sourceapps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.tsapps/dashboard/src/api/conversations.tsapps/dashboard/src/components/conversations/constants.tsapps/dashboard/src/components/conversations/conversation-detail.tsxapps/dashboard/src/components/conversations/conversation-overview.tsxapps/dashboard/src/components/conversations/conversation-status-badge.tsxapps/dashboard/src/components/conversations/conversation-table-row.tsxapps/dashboard/src/components/conversations/conversation-timeline.tsxapps/dashboard/src/components/conversations/conversations-content.tsxapps/dashboard/src/components/conversations/conversations-empty-state.tsxapps/dashboard/src/components/conversations/conversations-filters.tsxapps/dashboard/src/components/conversations/conversations-table.tsxapps/dashboard/src/hooks/use-conversation-url-state.tsapps/dashboard/src/hooks/use-fetch-conversation-activities.tsapps/dashboard/src/hooks/use-fetch-conversations.tsapps/dashboard/src/main.tsxapps/dashboard/src/pages/activity-feed.tsxapps/dashboard/src/types/conversation.tsapps/dashboard/src/utils/query-keys.tsapps/dashboard/src/utils/routes.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts (1)
58-67:⚠️ Potential issue | 🟠 MajorValidate the agent against
conversation._agentIdbefore using its name.This lookup trusts
command.agentIdentifier, but the actual delivery still happens asconversation._agentId. If those diverge, the message is sent as one agent while the activity row records anotheragentId/senderName. It also does an extra query for resolve/signals flows wheresenderNameis never used.Suggested fix
- const agent = await this.agentRepository.findOne( - { - _environmentId: command.environmentId, - _organizationId: command.organizationId, - identifier: command.agentIdentifier, - }, - { name: 1, identifier: 1 } - ); - const agentName = agent?.name; + let agentName: string | undefined; + if (command.reply || command.update) { + const agent = await this.agentRepository.findOne( + { + _environmentId: command.environmentId, + _organizationId: command.organizationId, + _id: conversation._agentId, + identifier: command.agentIdentifier, + }, + { name: 1 } + ); + + if (!agent) { + throw new BadRequestException('Agent identifier does not match conversation agent'); + } + + agentName = agent.name; + }As per coding guidelines:
apps/api/**: Review with focus on security, authentication, and authorization. Check for proper error handling and input validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts` around lines 58 - 67, The code fetches an agent by command.agentIdentifier and uses agent?.name without verifying it matches the conversation actor, which can cause senderName/agentId divergence and performs unnecessary queries when senderName isn't used; update handle-agent-reply.usecase.ts so that you validate the fetched agent against conversation._agentId (or directly fetch by conversation._agentId) before using its name, only perform the agentRepository.findOne (or findById) when senderName will be used, and add a guard to handle mismatches (throw or fallback) so the activity row always records the correct agentId/senderName (refer to agentRepository.findOne, conversation._agentId, and the surrounding logic in handle-agent-reply.usecase).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts`:
- Around line 58-67: The code fetches an agent by command.agentIdentifier and
uses agent?.name without verifying it matches the conversation actor, which can
cause senderName/agentId divergence and performs unnecessary queries when
senderName isn't used; update handle-agent-reply.usecase.ts so that you validate
the fetched agent against conversation._agentId (or directly fetch by
conversation._agentId) before using its name, only perform the
agentRepository.findOne (or findById) when senderName will be used, and add a
guard to handle mismatches (throw or fallback) so the activity row always
records the correct agentId/senderName (refer to agentRepository.findOne,
conversation._agentId, and the surrounding logic in handle-agent-reply.usecase).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a43d61f4-05d8-4996-bfa0-9127e54fcfea
📒 Files selected for processing (1)
apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts
What changed? Why was the change needed?
What changed
Added a new Conversations page to the dashboard that enables users to browse, filter, and view conversation details. This includes backend support for agent name resolution in conversation activities and a complete frontend implementation with filtering, pagination, activity timeline, and detailed conversation viewing capabilities.
Affected areas
@novu/api-service: Updated the
HandleAgentReplyusecase to fetch agent details by identifier before delivering messages. The agent name is now resolved and passed todeliverMessage()to ensure conversation activities include the agent's display name via thesenderNamefield. Added an agent identity validation guard to prevent mismatched agent identifiers.@novu/dashboard: Introduced a comprehensive conversations feature with new API client methods (
getConversationsList,getConversation,getConversationActivities), React hooks for data fetching (useFetchConversations,useFetchConversation,useFetchConversationActivities), and UI components for the conversations page (filters, table with rows, detail panel, timeline, status badges, empty states). Added URL-synchronized filter state management viauseConversationUrlStatehook, supporting filters for date range, subscriber, provider, and conversation ID. Integrated the new conversations tab into the activity feed page with feature-flag support. Updated routes to include the new/activity/conversationspath.Submodule reference (.source): Updated to a new commit hash reflecting changes in the referenced subproject.
Key technical decisions
usePersistedPageSizefor consistent UX across sessions.Testing
No test files were added in this PR. Manual verification of the conversations page UI, filtering behavior, pagination, and timeline rendering would be necessary.
Screenshots
Expand for optional sections
Related enterprise PR
Special notes for your reviewer