Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions app/src/components/skills/MeetingBotsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } fro
import { type MascotFace, RiveMascot } from '../../features/human/Mascot';
import { useT } from '../../lib/i18n/I18nContext';
import {
isCapacityGateMessage,
joinMeetViaBackendBot,
leaveBackendMeetBot,
listMeetCalls,
Expand Down Expand Up @@ -260,7 +261,12 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
if (!hasSubmittedRef.current) return;
if (meetStatus === 'error') {
hasSubmittedRef.current = false;
const message = meetError?.trim() || t('skills.meetingBots.failedToStart');
const raw = meetError?.trim() || t('skills.meetingBots.failedToStart');
// A capacity-gate error carries the backend's terse "…try again later."
// wording; show the tailored, actionable (and localized) copy instead (#4151).
const message = isCapacityGateMessage(raw)
? t('skills.meetingBots.serverOverloaded')
: raw;
setError(message);
setSubmitting(false);
onToast?.({ type: 'error', title: t('skills.meetingBots.couldNotStartTitle'), message });
Expand Down Expand Up @@ -288,7 +294,10 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
listenOnly,
});
} catch (err) {
const message = err instanceof Error ? err.message : t('skills.meetingBots.failedToStart');
const raw = err instanceof Error ? err.message : t('skills.meetingBots.failedToStart');
const message = isCapacityGateMessage(raw)
? t('skills.meetingBots.serverOverloaded')
: raw;
setError(message);
setSubmitting(false);
hasSubmittedRef.current = false;
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5033,6 +5033,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'تكبير',
'skills.meetingBots.sendTo': 'إرسال إلى',
'skills.meetingBots.serverOverloaded':
'يشهد OpenHuman ضغطًا كبيرًا في الوقت الحالي. يُرجى المحاولة مرة أخرى بعد بضع دقائق.',
'skills.meetingBots.soonSuffix': 'قريبًا',
'skills.meetingBots.starting': 'جارٍ البدء…',
'skills.meetingBots.recentCallsAriaLabel': 'مكالمات الاجتماعات الأخيرة',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5136,6 +5136,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'মাইক্রোসফট টিম',
'skills.meetingBots.platforms.zoom': 'জুম',
'skills.meetingBots.sendTo': 'পাঠান',
'skills.meetingBots.serverOverloaded':
'OpenHuman এই মুহূর্তে অত্যধিক চাপের মধ্যে রয়েছে। অনুগ্রহ করে কয়েক মিনিট পরে আবার চেষ্টা করুন।',
'skills.meetingBots.soonSuffix': 'শীঘ্রই',
'skills.meetingBots.starting': 'শুরু হচ্ছে…',
'skills.meetingBots.recentCallsAriaLabel': 'সাম্প্রতিক মিটিং কল',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5267,6 +5267,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Senden an',
'skills.meetingBots.serverOverloaded':
'OpenHuman ist gerade stark ausgelastet. Bitte versuche es in ein paar Minuten erneut.',
'skills.meetingBots.soonSuffix': 'bald',
'skills.meetingBots.starting': 'Beginnend mit …',
'skills.meetingBots.recentCallsAriaLabel': 'Letzte Meeting-Anrufe',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5783,6 +5783,8 @@ const en: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Send to {label}',
'skills.meetingBots.serverOverloaded':
'OpenHuman is under heavy load right now. Please try again in a few minutes.',
'skills.meetingBots.soonSuffix': 'soon',
'skills.meetingBots.starting': 'Starting…',
'skills.meetingBots.recentCallsAriaLabel': 'Recent meeting calls',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5231,6 +5231,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Equipos de Microsoft',
'skills.meetingBots.platforms.zoom': 'Ampliar',
'skills.meetingBots.sendTo': 'Enviar a',
'skills.meetingBots.serverOverloaded':
'OpenHuman tiene una carga muy alta en este momento. Inténtalo de nuevo en unos minutos.',
'skills.meetingBots.soonSuffix': 'pronto',
'skills.meetingBots.starting': 'Iniciando…',
'skills.meetingBots.recentCallsAriaLabel': 'Llamadas de reunión recientes',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5252,6 +5252,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Envoyer à',
'skills.meetingBots.serverOverloaded':
'OpenHuman est actuellement très sollicité. Veuillez réessayer dans quelques minutes.',
'skills.meetingBots.soonSuffix': 'bientôt',
'skills.meetingBots.starting': 'Démarrage…',
'skills.meetingBots.recentCallsAriaLabel': 'Appels de réunion récents',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5139,6 +5139,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'माइक्रोसॉफ्ट टीमें',
'skills.meetingBots.platforms.zoom': 'ज़ूम करें',
'skills.meetingBots.sendTo': 'भेजें',
'skills.meetingBots.serverOverloaded':
'OpenHuman अभी भारी लोड में है। कृपया कुछ मिनटों में फिर से प्रयास करें।',
'skills.meetingBots.soonSuffix': 'जल्द ही',
'skills.meetingBots.starting': 'शुरू हो रहा है…',
'skills.meetingBots.recentCallsAriaLabel': 'हाल की मीटिंग कॉल',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5154,6 +5154,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Kirim ke',
'skills.meetingBots.serverOverloaded':
'OpenHuman sedang menerima beban tinggi saat ini. Silakan coba lagi dalam beberapa menit.',
'skills.meetingBots.soonSuffix': 'segera',
'skills.meetingBots.starting': 'Memulai...',
'skills.meetingBots.recentCallsAriaLabel': 'Panggilan rapat terbaru',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5220,6 +5220,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Invia a',
'skills.meetingBots.serverOverloaded':
'OpenHuman è sottoposto a un carico elevato in questo momento. Riprova tra qualche minuto.',
'skills.meetingBots.soonSuffix': 'presto',
'skills.meetingBots.starting': 'Avvio…',
'skills.meetingBots.recentCallsAriaLabel': 'Chiamate recenti delle riunioni',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5089,6 +5089,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': '보내기',
'skills.meetingBots.serverOverloaded':
'현재 OpenHuman의 부하가 매우 높습니다. 몇 분 후에 다시 시도해 주세요.',
'skills.meetingBots.soonSuffix': '곧',
'skills.meetingBots.starting': '시작 중…',
'skills.meetingBots.recentCallsAriaLabel': '최근 회의 통화',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5208,6 +5208,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Wyślij do',
'skills.meetingBots.serverOverloaded':
'OpenHuman jest teraz mocno obciążony. Spróbuj ponownie za kilka minut.',
'skills.meetingBots.soonSuffix': 'wkrótce',
'skills.meetingBots.starting': 'Uruchamianie…',
'skills.meetingBots.recentCallsAriaLabel': 'Ostatnie rozmowy na spotkaniach',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5223,6 +5223,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Enviar para',
'skills.meetingBots.serverOverloaded':
'O OpenHuman está sob carga intensa no momento. Tente novamente em alguns minutos.',
'skills.meetingBots.soonSuffix': 'em breve',
'skills.meetingBots.starting': 'Iniciando…',
'skills.meetingBots.recentCallsAriaLabel': 'Chamadas de reunião recentes',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5182,6 +5182,8 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
'skills.meetingBots.platforms.zoom': 'Zoom',
'skills.meetingBots.sendTo': 'Отправить',
'skills.meetingBots.serverOverloaded':
'OpenHuman сейчас сильно загружен. Пожалуйста, повторите попытку через несколько минут.',
'skills.meetingBots.soonSuffix': 'скоро',
'skills.meetingBots.starting': 'Запуск…',
'skills.meetingBots.recentCallsAriaLabel': 'Недавние звонки на встречах',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4883,6 +4883,7 @@ const messages: TranslationMap = {
'skills.meetingBots.platforms.teams': '微软团队',
'skills.meetingBots.platforms.zoom': '变焦',
'skills.meetingBots.sendTo': '发送到会议',
'skills.meetingBots.serverOverloaded': 'OpenHuman 当前负载过高,请几分钟后重试。',
'skills.meetingBots.soonSuffix': '很快',
'skills.meetingBots.starting': '启动中…',
'skills.meetingBots.recentCallsAriaLabel': '最近的会议通话',
Expand Down
30 changes: 28 additions & 2 deletions app/src/services/__tests__/joinMeetingViaMascotBot.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import {
isCapacityGateMessage,
joinMeetingViaMascotBot,
type MascotJoinMeetingError,
SERVER_OVERLOADED_MESSAGE,
Expand Down Expand Up @@ -49,15 +50,22 @@ describe('joinMeetingViaMascotBot', () => {
});
});

it('flags SERVER_OVERLOADED responses with isCapacityGated=true', async () => {
postMock.mockRejectedValueOnce({ success: false, error: SERVER_OVERLOADED_MESSAGE });
it('flags the real backend SERVER_OVERLOADED wording and shows the tailored copy (#4151)', async () => {
// The VERBATIM backend message (backend src/utils/paidPlan.ts) — NOT the
// frontend's friendly constant. Mocking the real wire string is what makes
// this test catch the prior exact-equality mismatch that leaked the generic
// "…try again later." text to users.
const BACKEND_CAPACITY_MESSAGE =
'Mascot streaming capacity is exhausted. Please try again later.';
postMock.mockRejectedValueOnce({ success: false, error: BACKEND_CAPACITY_MESSAGE });
let caught: MascotJoinMeetingError | undefined;
try {
await joinMeetingViaMascotBot({ platform: 'gmeet', meetUrl: 'https://meet.google.com/abc' });
} catch (e) {
caught = e as MascotJoinMeetingError;
}
expect(caught?.isCapacityGated).toBe(true);
// The tailored, actionable copy is surfaced — not the raw backend string.
expect(caught?.message).toBe(SERVER_OVERLOADED_MESSAGE);
});

Expand All @@ -75,3 +83,21 @@ describe('joinMeetingViaMascotBot', () => {
).rejects.toMatchObject({ isCapacityGated: false, message: 'network down' });
});
});

describe('isCapacityGateMessage', () => {
it('matches the real backend capacity wording (and case/wording drift)', () => {
expect(
isCapacityGateMessage('Mascot streaming capacity is exhausted. Please try again later.')
).toBe(true);
expect(isCapacityGateMessage('MASCOT STREAMING CAPACITY IS EXHAUSTED.')).toBe(true);
expect(isCapacityGateMessage('Streaming capacity reached — retry soon')).toBe(true);
});

it('does not match unrelated errors or empty input', () => {
expect(isCapacityGateMessage('Bad Request')).toBe(false);
expect(isCapacityGateMessage('network down')).toBe(false);
expect(isCapacityGateMessage('')).toBe(false);
expect(isCapacityGateMessage(null)).toBe(false);
expect(isCapacityGateMessage(undefined)).toBe(false);
});
});
36 changes: 31 additions & 5 deletions app/src/services/meetCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,34 @@ export interface MascotJoinMeetingResult {
}

/**
* The 429 capacity-gate message the backend emits for free users. Treated
* as the canonical user-facing copy so the UI can show a tailored notice
* without leaking the underlying paid-plan rule.
* Tailored, actionable user-facing copy shown when the backend's capacity gate
* trips — replaces the backend's terse "…Please try again later." with retry
* guidance, without leaking the underlying paid-plan rule.
*/
export const SERVER_OVERLOADED_MESSAGE =
'OpenHuman is under heavy load right now. Please try again in a few minutes.';

/**
* Recognize the backend's free-user capacity-gate response (`SERVER_OVERLOADED`,
* backend `paidPlan.ts` → `"Mascot streaming capacity is exhausted. Please try
* again later."`).
*
* The shared `apiClient` drops `errorCode` from error bodies (`apiClient.ts`
* only forwards `error` + `message`), so the message text is the only signal
* that survives. Detection therefore MUST key on the backend wording — it used
* to be compared for exact equality against [`SERVER_OVERLOADED_MESSAGE`], but
* that constant was changed to friendlier copy and no longer matches the
* backend string, so the check silently never fired and the raw generic
* "…try again later." leaked to the user instead of the tailored notice
* (#4151). Match a stable substring, case-insensitively, so minor wording drift
* on either side still resolves to the actionable message.
*/
export function isCapacityGateMessage(text: string | null | undefined): boolean {
if (!text) return false;
const t = text.toLowerCase();
return t.includes('streaming capacity') || t.includes('capacity is exhausted');
}

export interface MascotJoinMeetingError {
/** User-safe error text. Falls back to a generic message. */
message: string;
Expand Down Expand Up @@ -379,8 +400,13 @@ export async function joinMeetingViaMascotBot(
: err instanceof Error
? err.message
: 'Failed to start meeting bot.';
const isCapacityGated = text === SERVER_OVERLOADED_MESSAGE;
const wrapped: MascotJoinMeetingError = { message: text, isCapacityGated };
const isCapacityGated = isCapacityGateMessage(text);
// When capacity-gated, surface the tailored, actionable copy instead of the
// backend's raw "…try again later." string (#4151).
const wrapped: MascotJoinMeetingError = {
message: isCapacityGated ? SERVER_OVERLOADED_MESSAGE : text,
isCapacityGated,
};
throw wrapped;
}
}
Loading