Skip to content

Commit 6291567

Browse files
authored
Add resources reminder (#141)
* feat: WIP resources reminder * refactor: rm tooltip for nav items * test(pages/assistant): new test * feat(pages/assistant): use ref to track resources * refactor(pages/assistant.tsx): text change * test(pages/assistant): fix * refactor(pages/assistant.tsx): text change * fix(pages/assistant.tsx): aria-label * test(pages/assistant): update
1 parent b2eb74a commit 6291567

4 files changed

Lines changed: 174 additions & 3 deletions

File tree

__tests__/pages/assistant.test.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,4 +2711,107 @@ describe('EventAssistantRoom', () => {
27112711
expect(getPollResponseCounts).not.toHaveBeenCalled();
27122712
});
27132713
});
2714+
2715+
describe('Conversation almost over notification', () => {
2716+
const endTime = new Date(Date.now() + 10 * 60 * 1000).toISOString(); // 10 minutes from now
2717+
2718+
beforeEach(async () => {
2719+
const mockResources = [
2720+
{ id: 'res-1', source: 'ai', category: 'suggested', title: 'Book A', participantVisible: true },
2721+
{ id: 'res-2', source: 'ai', category: 'suggested', title: 'Book B', participantVisible: true },
2722+
];
2723+
2724+
(RetrieveData as jest.Mock).mockImplementation((path: string) => {
2725+
if (path.startsWith('conversations/')) {
2726+
return Promise.resolve({ agents: [{ id: 'agent-123', agentType: 'eventAssistant' }] });
2727+
}
2728+
return Promise.resolve([]);
2729+
});
2730+
2731+
(createConversationFromData as jest.Mock).mockResolvedValue({
2732+
agents: [{ id: 'agent-123', agentType: 'eventAssistant' }],
2733+
type: { name: 'eventAssistant' },
2734+
endTime,
2735+
resources: mockResources,
2736+
});
2737+
2738+
await act(async () => {
2739+
render(<EventAssistantRoom authType={'guest'} />);
2740+
});
2741+
2742+
await waitFor(() => expect(createConversationFromData).toHaveBeenCalled());
2743+
await waitFor(() => expect(mockSocket.on).toHaveBeenCalledWith('conversation:ending', expect.any(Function)));
2744+
});
2745+
it('displays a banner, reminding user to access resources if they never checked them', async () => {
2746+
// Check the handler was registered and simulate the event
2747+
const endingHandlerCall = mockSocket.on.mock.calls.find(([event]: [string]) => event === 'conversation:ending');
2748+
const endingHandler = endingHandlerCall?.[1];
2749+
expect(endingHandler).toBeDefined();
2750+
2751+
act(() => {
2752+
endingHandler({ endTime });
2753+
});
2754+
2755+
await waitFor(() => {
2756+
expect(
2757+
screen.getByText(
2758+
"This event ends soon. Don't forget to check the Resources tab for follow-up readings worth bookmarking.",
2759+
),
2760+
).toBeInTheDocument();
2761+
});
2762+
2763+
// Simulate clicking the close button to dismiss the banner
2764+
const closeButton = screen.getByRole('button', { name: /dismiss resources reminder/i });
2765+
await act(async () => {
2766+
await userEvent.click(closeButton);
2767+
});
2768+
2769+
await waitFor(() => {
2770+
expect(
2771+
screen.queryByText(
2772+
"This event ends soon. Don't forget to check the Resources tab for follow-up readings worth bookmarking.",
2773+
),
2774+
).not.toBeInTheDocument();
2775+
});
2776+
});
2777+
2778+
it('displays a banner, reminding user to access resources if they did previously check them', async () => {
2779+
// Click the Resources tab to simulate user checking resources
2780+
const resourcesTab = await screen.getAllByLabelText('Resources')[0];
2781+
await act(async () => {
2782+
await userEvent.click(resourcesTab);
2783+
});
2784+
2785+
// Check the handler was registered and simulate the event
2786+
const endingHandlerCall = mockSocket.on.mock.calls.find(([event]: [string]) => event === 'conversation:ending');
2787+
const endingHandler = endingHandlerCall?.[1];
2788+
expect(endingHandler).toBeDefined();
2789+
2790+
act(() => {
2791+
endingHandler({ endTime });
2792+
});
2793+
2794+
await waitFor(() => {
2795+
expect(
2796+
screen.getByText(
2797+
"This event ends soon. Don't forget to review Resources and bookmark or save them for your reference.",
2798+
),
2799+
).toBeInTheDocument();
2800+
});
2801+
2802+
// Simulate clicking the close button to dismiss the banner
2803+
const closeButton = screen.getByRole('button', { name: /dismiss resources reminder/i });
2804+
await act(async () => {
2805+
await userEvent.click(closeButton);
2806+
});
2807+
2808+
await waitFor(() => {
2809+
expect(
2810+
screen.queryByText(
2811+
"This event ends soon. Don't forget to review Resources and bookmark or save them for your reference.",
2812+
),
2813+
).not.toBeInTheDocument();
2814+
});
2815+
});
2816+
});
27142817
});

components/NavigationBar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ interface NavigationBarProps {
3939
* - Mobile: fixed bottom bar, horizontal layout
4040
* - Desktop: left sidebar, vertical layout
4141
*
42-
* Three items: Event Bot, Group Chat, Transcript.
42+
* Four items: Event Bot, Group Chat, Transcript, Resources.
4343
* Icons are black when selected, grey when unselected, on a light purple background.
4444
*/
4545
export function NavigationBar({
@@ -180,7 +180,9 @@ export function NavigationBar({
180180
aria-label="Main navigation"
181181
>
182182
{navItems.map((item) => (
183-
<NavButton key={item.id} item={item} size={26} />
183+
<div key={item.id} className="relative w-full flex justify-center">
184+
<NavButton item={item} size={26} />
185+
</div>
184186
))}
185187
</nav>
186188

globals.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
--color-medium-slate-blue: #4845d2;
3737
--color-neutral-900: #0b0d0e;
3838
--color-light-blue: #a5b4fc;
39+
--animate-slide-in: slideIn 0.5s ease-out forwards;
3940

4041
@keyframes rotate-bg {
4142
from {
@@ -45,4 +46,15 @@
4546
transform: rotate(360deg);
4647
}
4748
}
49+
50+
@keyframes slideIn {
51+
0% {
52+
transform: translateY(-100%);
53+
opacity: 0;
54+
}
55+
100% {
56+
transform: translateY(0);
57+
opacity: 1;
58+
}
59+
}
4860
}

pages/assistant.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState, useRef, useCallback } from 'react';
22
import { useRouter } from 'next/router';
3+
import CloseIcon from '@mui/icons-material/Close';
34

45
import { AssistantChatPanel } from '../components/AssistantChatPanel';
56
import { GroupChatPanel } from '../components/GroupChatPanel';
@@ -30,6 +31,7 @@ import { useSessionJoin } from '../utils/useSessionJoin';
3031
import { NavigationBar, NavTab } from '../components/NavigationBar';
3132
import { PreferencesPanel } from '../components/PreferencesPanel';
3233
import { getFeedbackEligibleMessages } from '../utils/feedbackEligibility';
34+
import { Button } from '@mui/material';
3335

3436
export const getServerSideProps = async (context: { req: any }) => {
3537
return CheckAuthHeader(context.req.headers);
@@ -115,8 +117,10 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
115117
const hasJoinedConvRef = useRef(false);
116118
const [agentId, setAgentId] = useState<string | null>(null);
117119
const [agentActive, setAgentActive] = useState<boolean>(true);
120+
const [resourcesReminderActive, setResourcesReminderActive] = useState<boolean>(false);
118121
const [agentIds, setAgentIds] = useState<string[]>([]);
119122
const [conversationFeatures, setConversationFeatures] = useState<{ name: string; enabled?: boolean }[]>([]);
123+
120124
const conversationType = useConversationType();
121125
const setConversationType = useSetConversationType();
122126
const setBotNameContext = useSetBotName();
@@ -147,6 +151,9 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
147151
// Ref to track active tab for socket handler
148152
const activeTabRef = useRef<NavTab>('assistant');
149153

154+
// Ref to track resources value
155+
const resourcesRef = useRef<Resource[]>(resources);
156+
150157
// Use custom hook for session joining
151158
const { socket, pseudonym, userId, isConnected, errorMessage: sessionError, lastReconnectTime } = useSessionJoin();
152159

@@ -235,21 +242,33 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
235242
});
236243
};
237244

245+
const conversationEndingHandler = () => {
246+
console.log('conversation:ending received');
247+
248+
// Don't activate the resources reminder if no resources are available
249+
if (resourcesRef.current.length === 0) return;
250+
setResourcesReminderActive(true);
251+
};
252+
238253
const pollChoiceHandler = (data: { pollId: string; counts: Record<string, number> }) => {
239254
if (!data?.pollId || !data?.counts) return;
240255
setPollCounts((prev) => ({ ...prev, [data.pollId]: data.counts }));
241256
};
242257

243258
socket.on('message:new', messageHandler);
244259
socket.on('resources:updated', resourcesUpdatedHandler);
260+
socket.on('conversation:ending', conversationEndingHandler);
245261
socket.on('choice:new', pollChoiceHandler);
246262

263+
console.log('Socket event listeners registered');
264+
247265
return () => {
248266
socket.off('message:new', messageHandler);
249267
socket.off('resources:updated', resourcesUpdatedHandler);
268+
socket.off('conversation:ending', conversationEndingHandler);
250269
socket.off('choice:new', pollChoiceHandler);
251270
};
252-
}, [socket]);
271+
}, [socket, resourcesNavBadgeDismissed, resources.length]);
253272

254273
// Keep activeTabRef in sync with activeTab state
255274
useEffect(() => {
@@ -315,6 +334,7 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
315334
}
316335

317336
if (Array.isArray(conversation?.resources)) {
337+
console.log('Fetched conversation resources:', conversation.resources);
318338
setResources(conversation.resources);
319339
}
320340

@@ -757,6 +777,11 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
757777
return () => window.removeEventListener('keydown', handleKeyDown);
758778
}, [controlledMode]);
759779

780+
// Keep resourcesRef in sync with resources state
781+
useEffect(() => {
782+
resourcesRef.current = resources;
783+
}, [resources]);
784+
760785
const sendFeedbackRating = async (messageId: string, rating: string) => {
761786
const conversationId = router.query.conversationId as string;
762787
trackConversationEvent(conversationId, 'assistant', 'rating_submitted', rating);
@@ -873,6 +898,35 @@ function EventAssistantRoom({ authType: _authType }: { authType: AuthType }) {
873898
<>
874899
{/* Chat / Assistant / Resources / Preferences panel */}
875900
<div className="flex-1 flex flex-col relative overflow-hidden">
901+
{/* Show dismissable resources reminder if active */}
902+
{resourcesReminderActive && (
903+
<div className="absolute top-0 w-full z-10 bg-yellow-100 p-4 rounded shadow-2xl animate-slide-in">
904+
<div className="flex justify-between font-bold">
905+
<p>
906+
{resourcesNavBadgeDismissed
907+
? "This event ends soon. Don't forget to review Resources and bookmark or save them for your reference."
908+
: "This event ends soon. Don't forget to check the Resources tab for follow-up readings worth bookmarking."}
909+
</p>
910+
<Button
911+
aria-label="Dismiss resources reminder"
912+
className="ml-4 px-2 py-2"
913+
onClick={() => setResourcesReminderActive(false)}
914+
color="error"
915+
>
916+
<CloseIcon />
917+
</Button>
918+
</div>
919+
<button
920+
className="mt-2 px-3 py-1 bg-yellow-200 hover:bg-yellow-300 rounded"
921+
onClick={() => {
922+
handleTabChange('resources');
923+
setResourcesReminderActive(false);
924+
}}
925+
>
926+
View Resources
927+
</button>
928+
</div>
929+
)}
876930
{router.query.view === 'preferences' ? (
877931
<PreferencesPanel botName={botName} />
878932
) : isConnected ? (

0 commit comments

Comments
 (0)