Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR makes manual AI→human handoffs more explicit by sending and surfacing a clear handoff reason, while also updating backend persistence behavior and extending tests/UI to reflect the new handoff context.
Changes:
- Widget now passes an explicit
reasonwhen the visitor clicks “Talk to human”. - Backend
handoffToHumanupdates conversation metadata (lastMessageAt,unreadByAgent) and schedules a notification. - Inbox UI displays handoff context (banner + empty-AI-review handoff explanation), and tests are updated accordingly.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/convex/convex/aiAgent.ts | Updates handoff mutation to patch conversation metadata and attempt to notify on handoff. |
| packages/convex/tests/aiAgentHandoffPath.test.ts | Adds assertions to ensure the new persistence/notification behavior exists in source. |
| apps/widget/src/components/ConversationView.tsx | Sends explicit handoff reason for the manual “Talk to human” action. |
| apps/widget/src/components/ConversationView.test.tsx | Refactors action/mutation mocks and adds a test asserting the explicit reason is passed. |
| apps/web/src/app/inbox/page.tsx | Shows handoff reason context in the inbox header and AI review panel. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/convex/convex/aiAgent.ts
Outdated
| await ctx.scheduler.runAfter(0, internal.notifications.notifyNewMessage, { | ||
| conversationId: args.conversationId, | ||
| messageContent: handoffMessage, | ||
| senderType: "bot", | ||
| messageId, | ||
| senderId: "ai-agent", | ||
| sentAt: now, | ||
| channel: "chat", | ||
| }); | ||
|
|
There was a problem hiding this comment.
internal.notifications.notifyNewMessage only notifies agents when senderType === "visitor"; when senderType is "bot" it routes notifications to the visitor instead (push/email). As written, the handoff will likely notify the visitor (potentially redundantly) and won’t alert agents about the handoff/new message. Consider routing an agent-facing event explicitly (e.g., internal.notifications.routeEvent with audience: "agent") or extending notifications to support an agent-notify mode for bot/handoff events, rather than calling notifyNewMessage with senderType: "bot".
| await ctx.scheduler.runAfter(0, internal.notifications.notifyNewMessage, { | |
| conversationId: args.conversationId, | |
| messageContent: handoffMessage, | |
| senderType: "bot", | |
| messageId, | |
| senderId: "ai-agent", | |
| sentAt: now, | |
| channel: "chat", | |
| }); | |
| await ctx.scheduler.runAfter(0, internal.notifications.routeEvent, { | |
| conversationId: args.conversationId, | |
| audience: "agent", | |
| type: "new_message", | |
| messageId, | |
| sentAt: now, | |
| }); |
| const mockedUseMutation = useMutation as unknown as ReturnType<typeof vi.fn>; | ||
| mockedUseMutation.mockReturnValue(vi.fn().mockResolvedValue(undefined)); | ||
| mockedUseMutation.mockImplementation((mutationRef: unknown) => { | ||
| if (mutationRef === "messages.send") { | ||
| return sendMessageMutationMock; | ||
| } | ||
| if (mutationRef === "visitors.identify") { | ||
| return identifyVisitorMutationMock; | ||
| } | ||
| if (mutationRef === "aiAgent.submitFeedback") { | ||
| return submitAiFeedbackMutationMock; | ||
| } | ||
| if (mutationRef === "aiAgent.handoffToHuman") { | ||
| return handoffToHumanMutationMock; | ||
| } | ||
| return vi.fn().mockResolvedValue(undefined); | ||
| }); |
There was a problem hiding this comment.
This useMutation mockImplementation expects mutationRef === "messages.send" to return sendMessageMutationMock, but the @opencom/convex mock in this test file doesn’t define api.messages.send (only messages.list). That means the component will call useMutation(undefined) and this branch will never match, so sendMessageMutationMock is unused and the test setup diverges from real usage. Add messages.send: "messages.send" to the mocked api object so the mutation ref mapping is exercised correctly.
| <span className="truncate"> | ||
| {selectedConversation.aiWorkflow.handoffReason ?? | ||
| "Reason not provided by handoff trigger"} | ||
| </span> |
There was a problem hiding this comment.
The fallback copy "Reason not provided by handoff trigger" is duplicated in multiple UI spots (banner + AI review empty state). Consider centralizing this string (e.g., a constant/helper) so future copy changes stay consistent across the inbox UI.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| status: "open", | ||
| updatedAt: now, | ||
| lastMessageAt: now, | ||
| unreadByAgent: (conversation.unreadByAgent || 0) + 1, | ||
| aiWorkflowState: "handoff", |
There was a problem hiding this comment.
lastMessageAt/updatedAt are set from now, but the inserted handoff message uses its own Date.now() value for createdAt. This can create small ordering inconsistencies (conversation sorted by lastMessageAt slightly after the message timestamp). Consider capturing a single timestamp once and using it for both the message createdAt and the conversation patch fields.
| expect(source).toContain('senderId: "ai-agent"'); | ||
| expect(source).not.toContain('senderId: "system"'); | ||
| expect(source).toContain("lastMessageAt: now"); | ||
| expect(source).toContain("unreadByAgent: (conversation.unreadByAgent || 0) + 1"); |
There was a problem hiding this comment.
This test is asserting an exact source-code substring for the unreadByAgent update expression. That makes the test very sensitive to harmless refactors/formatting changes (e.g., switching to ??, adding whitespace, or extracting a helper) while not actually validating runtime behavior. Consider loosening the assertion (e.g., check for unreadByAgent: and + 1 separately / use a regex) or adding an integration-style assertion that the mutation actually increments unreadByAgent and updates lastMessageAt.
| expect(source).toContain("unreadByAgent: (conversation.unreadByAgent || 0) + 1"); | |
| expect(source).toMatch(/unreadByAgent:\s*[^+]*\+\s*1/); |
This pull request introduces improvements to the AI handoff workflow and enhances both backend logic and frontend user experience. The changes ensure that a clear handoff reason is recorded and displayed when a visitor requests to talk to a human, and that the backend properly updates conversation status and notifies agents. Additionally, the test suite is updated to verify the new behavior.
AI Handoff Workflow and Reason Handling
"Visitor clicked Talk to human button") to the backend when triggering the handoff mutation (MANUAL_HANDOFF_REASONinConversationView.tsx). [1] [2]handoffToHumannow updates the conversation fields: setslastMessageAt, incrementsunreadByAgent, and triggers an internal notification to alert agents of the new message.Frontend UI Improvements
Test Suite Enhancements