diff --git a/app/src/components/skills/MeetingBotsCard.tsx b/app/src/components/skills/MeetingBotsCard.tsx index ed5243c53..861738232 100644 --- a/app/src/components/skills/MeetingBotsCard.tsx +++ b/app/src/components/skills/MeetingBotsCard.tsx @@ -21,6 +21,7 @@ import { resetBackendMeet, selectBackendMeetLastHarness, selectBackendMeetLastReply, + selectBackendMeetListenOnly, selectBackendMeetStatus, selectBackendMeetUrl, setBackendMeetJoining, @@ -83,6 +84,7 @@ function ActiveMeetingView({ onToast }: Props) { const dispatch = useAppDispatch(); const status = useAppSelector(selectBackendMeetStatus); const meetUrl = useAppSelector(selectBackendMeetUrl); + const listenOnly = useAppSelector(selectBackendMeetListenOnly); const lastReply = useAppSelector(selectBackendMeetLastReply); const lastHarness = useAppSelector(selectBackendMeetLastHarness); const face = faceFromMeetState(status, lastReply, lastHarness); @@ -114,14 +116,18 @@ function ActiveMeetingView({ onToast }: Props) { } }; - const statusText = - { + const statusText = (() => { + const base: Record = { joining: t('skills.meetingBots.liveStatusJoining'), - active: t('skills.meetingBots.liveStatusActive'), + active: listenOnly + ? t('skills.meetingBots.liveStatusListening') + : t('skills.meetingBots.liveStatusActive'), ended: t('skills.meetingBots.liveStatusEnded'), error: t('skills.meetingBots.liveStatusError'), idle: '', - }[status] ?? ''; + }; + return base[status] ?? ''; + })(); const canLeave = status === 'active' || status === 'joining'; const isDone = status === 'ended' || status === 'error'; @@ -242,6 +248,7 @@ export function MeetingBotsModal({ onClose, onToast }: ModalProps) { const { t } = useT(); const dispatch = useAppDispatch(); const [meetUrl, setMeetUrl] = useState(''); + const [respondTo, setRespondTo] = useState(''); const personaDisplayName = useAppSelector(selectPersonaDisplayName); const personaDescription = useAppSelector(selectPersonaDescription); const selectedMascotId = useAppSelector(selectSelectedMascotId); @@ -299,9 +306,12 @@ export function MeetingBotsModal({ onClose, onToast }: ModalProps) { setError(null); setSubmitting(true); try { + // Generate a correlation ID so every backend event for this session + // can be tied back to this meeting. + const meetingId = crypto.randomUUID(); // Optimistically update Redux state so the banner transitions to // the ActiveMeetingView immediately, before the backend responds. - dispatch(setBackendMeetJoining({ meetUrl: meetUrl.trim() })); + dispatch(setBackendMeetJoining({ meetUrl: meetUrl.trim(), meetingId })); // Backend Recall.ai bot: sends the mascot into the meeting via // the backend's Recall.ai integration. The backend joins as a // participant, renders the mascot as the bot's camera feed, and @@ -314,6 +324,8 @@ export function MeetingBotsModal({ onClose, onToast }: ModalProps) { systemPrompt, mascotId, riveColors, + correlationId: meetingId, + respondToParticipant: respondTo.trim() || undefined, }); onToast?.({ type: 'success', @@ -380,6 +392,26 @@ export function MeetingBotsModal({ onClose, onToast }: ModalProps) { /> + + {error && (