feat(meet): auto-fill meeting display name from connected account (#4297)

This commit is contained in:
Cyrus Gray
2026-06-29 20:27:20 +05:30
committed by GitHub
parent d867c1f113
commit 59502c21e2
+64 -2
View File
@@ -2,6 +2,8 @@ import debug from 'debug';
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { type MascotFace, RiveMascot } from '../../features/human/Mascot';
import { useComposioIntegrations } from '../../lib/composio/hooks';
import type { ComposioConnection } from '../../lib/composio/types';
import { useT } from '../../lib/i18n/I18nContext';
import {
isCapacityGateMessage,
@@ -9,6 +11,7 @@ import {
leaveBackendMeetBot,
listMeetCalls,
type MeetCallRecord,
type MeetingPlatform,
} from '../../services/meetCallService';
import {
type BackendMeetHarnessEvent,
@@ -38,6 +41,46 @@ type Toast = { type: 'success' | 'error' | 'info'; title: string; message?: stri
const log = debug('meeting-bots');
// Composio only hands back a connected account's email — there is no separate
// display-name field on `ComposioConnection`. A meeting display name is almost
// always "First Last" derived from that account, so we best-effort humanize the
// email's local part (`first.last` → `First Last`).
function deriveDisplayNameFromEmail(email: string | undefined): string {
const localPart = email?.split('@')[0]?.trim();
if (!localPart) return '';
return localPart
.split(/[._-]+/)
.filter(Boolean)
.map(token => token.charAt(0).toUpperCase() + token.slice(1))
.join(' ');
}
// Per-platform priority of Composio toolkits to source the user's in-call
// display name from: the platform's own connected account first, then the
// mailbox, then blank. Slugs are canonical Composio slugs (see
// `canonicalizeComposioToolkitSlug`).
const NAME_SOURCE_TOOLKITS: Record<MeetingPlatform, string[]> = {
gmeet: ['googlemeet', 'gmail'],
zoom: ['zoom', 'gmail'],
teams: ['microsoft_teams', 'outlook', 'gmail'],
webex: ['webex', 'gmail'],
};
// Resolve a default "Your name in this meeting" for the given platform: walk
// that platform's toolkit priority (own account → mail → blank) and return the
// first connected account whose email yields a usable name; blank if none are
// connected. The single entry point the form calls.
function resolveMeetingDisplayName(
platform: MeetingPlatform,
connectionByToolkit: Map<string, ComposioConnection>
): string {
for (const slug of NAME_SOURCE_TOOLKITS[platform]) {
const name = deriveDisplayNameFromEmail(connectionByToolkit.get(slug)?.accountEmail);
if (name) return name;
}
return '';
}
interface Props {
onToast?: (toast: Toast) => void;
}
@@ -205,6 +248,9 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
// backend join payload as `respondToParticipant` → `respondTo`, which the
// meeting stream uses to gate in-call requests to this speaker only.
const [respondTo, setRespondTo] = useState('');
// Once the user types in the name field we stop auto-prefilling it, so a
// late-arriving Composio fetch (it polls) can never clobber manual input.
const respondToTouchedRef = useRef(false);
// Active (respond when addressed) vs listen-only (transcribe only). Defaults
// to active; the bot still only replies after being addressed by the wake
// phrase. Forwarded to the backend as `listenOnly` and mirrored into the
@@ -222,6 +268,19 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
const meetError = useAppSelector(selectBackendMeetError);
const [recentCalls, setRecentCalls] = useState<MeetCallRecord[] | null>(null);
const [recentError, setRecentError] = useState<string | null>(null);
// The meeting platform this form sends to (only Google Meet is wired up for
// now). Drives both the join payload and which connected accounts we source
// the default display name from.
const platform: MeetingPlatform = 'gmeet';
// Prefill "Your name in this meeting" from the connected account that best
// matches this platform (calendar → mail → platform-native).
const { connectionByToolkit } = useComposioIntegrations();
const resolvedDisplayName = resolveMeetingDisplayName(platform, connectionByToolkit);
useEffect(() => {
if (respondToTouchedRef.current || !resolvedDisplayName) return;
setRespondTo(prev => (prev.trim() ? prev : resolvedDisplayName));
}, [resolvedDisplayName]);
const refreshRecentCalls = useCallback(async () => {
setRecentError(null);
@@ -295,7 +354,7 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
await joinMeetViaBackendBot({
meetUrl,
displayName: agentName,
platform: 'gmeet',
platform,
agentName,
systemPrompt,
mascotId,
@@ -356,7 +415,10 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
autoComplete="off"
spellCheck={false}
value={respondTo}
onChange={e => setRespondTo(e.target.value)}
onChange={e => {
respondToTouchedRef.current = true;
setRespondTo(e.target.value);
}}
placeholder={t('skills.meetingBots.respondToParticipantHint')}
disabled={submitting}
required