mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(meet): auto-fill meeting display name from connected account (#4297)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user