agora-appbuilder-core 4.1.13-beta.1 → 4.1.13-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/template/customization-api/temp.ts +1 -9
  3. package/template/defaultConfig.js +2 -2
  4. package/template/package.json +1 -1
  5. package/template/src/assets/font-styles.css +4 -0
  6. package/template/src/assets/fonts/icomoon.ttf +0 -0
  7. package/template/src/assets/selection.json +1 -1
  8. package/template/src/atoms/ActionMenu.tsx +1 -1
  9. package/template/src/atoms/CustomIcon.tsx +1 -0
  10. package/template/src/atoms/Popup.tsx +1 -1
  11. package/template/src/components/Controls.tsx +22 -41
  12. package/template/src/components/whiteboard/StrokeWidthTool.tsx +1 -5
  13. package/template/src/components/whiteboard/WhiteboardButton.tsx +1 -9
  14. package/template/src/components/whiteboard/WhiteboardCanvas.tsx +1 -9
  15. package/template/src/components/whiteboard/WhiteboardConfigure.tsx +215 -95
  16. package/template/src/components/whiteboard/WhiteboardCursor.tsx +9 -17
  17. package/template/src/components/whiteboard/WhiteboardToolBox.tsx +1 -15
  18. package/template/src/components/whiteboard/WhiteboardView.tsx +1 -9
  19. package/template/src/components/whiteboard/WhiteboardWidget.tsx +1 -4
  20. package/template/src/components/whiteboard/WhiteboardWrapper.tsx +2 -3
  21. package/template/src/language/default-labels/videoCallScreenLabels.ts +9 -9
  22. package/template/src/pages/VideoCall.tsx +2 -1
  23. package/template/src/pages/video-call/ActionSheetContent.tsx +0 -7
  24. package/template/src/pages/video-call/SidePanelHeader.tsx +80 -63
  25. package/template/src/rtm-events/constants.ts +9 -0
  26. package/template/src/subComponents/caption/Caption.tsx +4 -20
  27. package/template/src/subComponents/caption/CaptionContainer.tsx +262 -250
  28. package/template/src/subComponents/caption/CaptionIcon.tsx +6 -4
  29. package/template/src/subComponents/caption/CaptionText.tsx +26 -20
  30. package/template/src/subComponents/caption/LanguageSelectorPopup.tsx +30 -142
  31. package/template/src/subComponents/caption/Transcript.tsx +77 -32
  32. package/template/src/subComponents/caption/TranscriptIcon.tsx +7 -6
  33. package/template/src/subComponents/caption/TranslateActionMenu.tsx +128 -0
  34. package/template/src/subComponents/caption/useCaption.tsx +645 -482
  35. package/template/src/subComponents/caption/useSTTAPI.tsx +25 -4
  36. package/template/src/subComponents/caption/useStreamMessageUtils.native.ts +1 -1
  37. package/template/src/subComponents/caption/useStreamMessageUtils.ts +1 -1
  38. package/template/src/subComponents/caption/utils.ts +48 -40
  39. package/template/src/components/whiteboard/FastBoardView.tsx +0 -227
@@ -1,6 +1,6 @@
1
1
  import {createHook} from 'customization-implementation';
2
2
  import React, {useContext} from 'react';
3
- import {LanguageType, getLanguageLabel, hasConfigChanged} from './utils';
3
+ import {LanguageType, getLanguageLabel} from './utils';
4
4
  import useSTTAPI, {STTAPIResponse} from './useSTTAPI';
5
5
  import {useLocalUid} from '../../../agora-rn-uikit';
6
6
  import {logger, LogSource} from '../../logger/AppBuilderLogger';
@@ -12,31 +12,56 @@ import {useString} from '../../utils/useString';
12
12
  import {
13
13
  sttStartError,
14
14
  sttUpdateError,
15
+ sttSpokenLanguageToastHeading,
16
+ sttSpokenLanguageToastSubHeading,
17
+ sttSpokenLanguageToastSubHeadingDataInterface,
15
18
  } from '../../language/default-labels/videoCallScreenLabels';
16
19
  import chatContext from '../../components/ChatContext';
20
+ import {useRoomInfo} from '../../components/room-info/useRoomInfo';
21
+ import {useContent} from 'customization-api';
22
+
23
+ // Types
24
+ type GlobalSttState = {
25
+ globalSttEnabled: boolean;
26
+ globalSpokenLanguage: LanguageType;
27
+ globalTranslationTargets: LanguageType[];
28
+ initiatorName?: string;
29
+ };
17
30
 
18
- type TranslationItem = {
19
- lang: string;
20
- text: string;
21
- isFinal: boolean;
31
+ type TargetChange = {
32
+ prev: LanguageType | null;
33
+ next: LanguageType | null;
34
+ reason?: 'user' | 'spoken-language-changed' | 'auto-start';
35
+ };
36
+
37
+ type SttQueueItem = {
38
+ state: GlobalSttState;
39
+ isLocal: boolean;
40
+ targetChange?: TargetChange;
22
41
  };
23
42
 
24
43
  export type LanguageTranslationConfig = {
25
- source: LanguageType[]; // 'en-US'
44
+ source: LanguageType[]; // ['en-US']
26
45
  targets: LanguageType[]; // ['zh-CN', 'ja-JP']
27
- autoPopulate?: boolean; // e.g. if auto-populated from others
28
46
  };
29
47
 
30
- export type CaptionViewMode = 'original-and-translated' | 'translated';
48
+ export type STTViewMode = 'original-and-translated' | 'translated';
49
+
50
+ type TranslationItem = {
51
+ lang: string;
52
+ text: string;
53
+ isFinal: boolean;
54
+ };
31
55
 
32
56
  export type TranscriptItem = {
57
+ name: string;
33
58
  uid: string;
34
59
  time: number;
35
60
  text: string;
36
61
  translations?: TranslationItem[];
37
62
  // Stores which translation language was active when this transcript was created
38
63
  // This preserves historical context when users switch translation languages mid-meeting
39
- selectedTranslationLanguage?: string;
64
+ selectedTranslationLanguage?: LanguageType;
40
65
  };
41
66
 
42
67
  type CaptionObj = {
@@ -47,6 +72,28 @@ type CaptionObj = {
47
72
  };
48
73
  };
49
74
 
75
+ // helper
76
+ // (sorted) version of the target list.
77
+ const normalizeTargets = (arr: LanguageType[]) => [...(arr || [])].sort();
78
+
79
+ /**
80
+ * This helpher compares the STT_GLOBAL_STATE
81
+ * When a new user joins a call, they replay all previously persisted
82
+ * STT_GLOBAL_STATE events sent by other participants, as STT_GLOBAL_STATE
83
+ * is a session persistance event
84
+ * Without this check, the joining user would:
85
+ * - run start/update api multiple times as it will read event
86
+ * from all users attributes
87
+ */
88
+ const isSameState = (prev: GlobalSttState, next: GlobalSttState) => {
89
+ return (
90
+ prev.globalSttEnabled === next.globalSttEnabled &&
91
+ prev.globalSpokenLanguage === next.globalSpokenLanguage &&
92
+ JSON.stringify(normalizeTargets(prev.globalTranslationTargets)) ===
93
+ JSON.stringify(normalizeTargets(next.globalTranslationTargets))
94
+ );
95
+ };
96
+
50
97
  export const CaptionContext = React.createContext<{
51
98
  // for caption btn state
52
99
  isCaptionON: boolean;
@@ -56,24 +103,26 @@ export const CaptionContext = React.createContext<{
56
103
  isSTTError: boolean;
57
104
  setIsSTTError: React.Dispatch<React.SetStateAction<boolean>>;
58
105
 
59
- // to check if stt is active in the call
106
+ // to check if stt is active in the call :derived from globalSttState
60
107
  isSTTActive: boolean;
61
- setIsSTTActive: React.Dispatch<React.SetStateAction<boolean>>;
108
+
109
+ // flag to check if STT dependencies are ready (all required data loaded)
110
+ // Used to disable caption/transcript buttons until system is fully initialized
111
+ sttDepsReady: boolean;
62
112
 
63
113
  // holds the language selection for stt (deprecated - use sttForm instead)
64
114
  // language: LanguageType[];
65
115
  // setLanguage: React.Dispatch<React.SetStateAction<LanguageType[]>>;
66
116
 
67
- translationConfig: LanguageTranslationConfig;
68
- setTranslationConfig: React.Dispatch<
69
- React.SetStateAction<LanguageTranslationConfig>
70
- >;
117
+ globalSttState: GlobalSttState;
118
+ confirmSpokenLanguageChange: (newLang: LanguageType) => Promise<void>;
119
+ confirmTargetLanguageChange: (newTargetLang: LanguageType) => Promise<void>;
71
120
 
72
- captionViewMode: CaptionViewMode;
73
- setCaptionViewMode: React.Dispatch<React.SetStateAction<CaptionViewMode>>;
121
+ captionViewMode: STTViewMode;
122
+ setCaptionViewMode: React.Dispatch<React.SetStateAction<STTViewMode>>;
74
123
 
75
- transcriptViewMode: CaptionViewMode;
76
- setTranscriptViewMode: React.Dispatch<React.SetStateAction<CaptionViewMode>>;
124
+ transcriptViewMode: STTViewMode;
125
+ setTranscriptViewMode: React.Dispatch<React.SetStateAction<STTViewMode>>;
77
126
 
78
127
  // holds meeting transcript
79
128
  meetingTranscript: TranscriptItem[];
@@ -83,12 +132,6 @@ export const CaptionContext = React.createContext<{
83
132
  isLangChangeInProgress: boolean;
84
133
  setIsLangChangeInProgress: React.Dispatch<React.SetStateAction<boolean>>;
85
134
 
86
- // holds status of translation language change process
87
- isTranslationChangeInProgress: boolean;
88
- setIsTranslationChangeInProgress: React.Dispatch<
89
- React.SetStateAction<boolean>
90
- >;
91
-
92
135
  // holds live captions
93
136
  captionObj: CaptionObj;
94
137
  setCaptionObj: React.Dispatch<React.SetStateAction<CaptionObj>>;
@@ -100,13 +143,9 @@ export const CaptionContext = React.createContext<{
100
143
  activeSpeakerRef: React.MutableRefObject<string>;
101
144
  prevSpeakerRef: React.MutableRefObject<string>;
102
145
 
103
- selectedTranslationLanguage: string;
104
- setSelectedTranslationLanguage: React.Dispatch<React.SetStateAction<string>>;
146
+ selectedTranslationLanguage: LanguageType;
105
147
  // Ref for translation language - prevents stale closures in callbacks
106
- selectedTranslationLanguageRef: React.MutableRefObject<string>;
107
- // Ref for translation config - prevents stale closures in callbacks
108
- translationConfigRef: React.MutableRefObject<LanguageTranslationConfig>;
109
-
148
+ selectedTranslationLanguageRef: React.MutableRefObject<LanguageType | null>;
110
149
  // Stores spoken languages of all remote users (userUid -> spoken language)
111
150
  // Used to auto-populate target languages for new users
112
151
  remoteSpokenLanguages: Record<string, LanguageType>;
@@ -114,14 +153,13 @@ export const CaptionContext = React.createContext<{
114
153
  React.SetStateAction<Record<string, LanguageType>>
115
154
  >;
116
155
 
117
- handleTranslateConfigChange: (
118
- inputTranslationConfig: LanguageTranslationConfig,
119
- ) => Promise<void>;
120
156
  startSTTBotSession: (
121
157
  newConfig: LanguageTranslationConfig,
122
158
  ) => Promise<STTAPIResponse>;
123
159
  updateSTTBotSession: (
124
160
  newConfig: LanguageTranslationConfig,
161
+ isLocal: boolean,
162
+ targetChange?: TargetChange,
125
163
  ) => Promise<STTAPIResponse>;
126
164
  stopSTTBotSession: () => Promise<void>;
127
165
 
@@ -133,14 +171,9 @@ export const CaptionContext = React.createContext<{
133
171
  isSTTError: false,
134
172
  setIsSTTError: () => {},
135
173
  isSTTActive: false,
136
- setIsSTTActive: () => {},
174
+ sttDepsReady: false,
137
175
  // language: ['en-US'],
138
176
  // setLanguage: () => {},
139
- translationConfig: {
140
- source: [],
141
- targets: [],
142
- },
143
- setTranslationConfig: () => {},
144
177
  captionViewMode: 'translated',
145
178
  setCaptionViewMode: () => {},
146
179
  transcriptViewMode: 'translated',
@@ -149,8 +182,6 @@ export const CaptionContext = React.createContext<{
149
182
  setMeetingTranscript: () => {},
150
183
  isLangChangeInProgress: false,
151
184
  setIsLangChangeInProgress: () => {},
152
- isTranslationChangeInProgress: false,
153
- setIsTranslationChangeInProgress: () => {},
154
185
  captionObj: {},
155
186
  setCaptionObj: () => {},
156
187
  isSTTListenerAdded: false,
@@ -158,16 +189,21 @@ export const CaptionContext = React.createContext<{
158
189
  activeSpeakerRef: {current: ''},
159
190
  prevSpeakerRef: {current: ''},
160
191
  selectedTranslationLanguage: '',
161
- setSelectedTranslationLanguage: () => {},
162
- selectedTranslationLanguageRef: {current: ''},
163
- translationConfigRef: {current: {source: [], targets: []}},
192
+ selectedTranslationLanguageRef: {current: null},
164
193
  remoteSpokenLanguages: {},
165
194
  setRemoteSpokenLanguages: () => {},
166
- handleTranslateConfigChange: async () => {},
167
195
  startSTTBotSession: async () => ({success: false}),
168
196
  updateSTTBotSession: async () => ({success: false}),
169
197
  stopSTTBotSession: async () => {},
170
198
  getBotOwnerUid: (botUid: string | number) => botUid,
199
+ globalSttState: {
200
+ globalSttEnabled: false,
201
+ globalSpokenLanguage: '',
202
+ globalTranslationTargets: [],
203
+ initiatorName: '',
204
+ },
205
+ confirmSpokenLanguageChange: async () => {},
206
+ confirmTargetLanguageChange: async () => {},
171
207
  });
172
208
 
173
209
  interface CaptionProviderProps {
@@ -179,317 +215,224 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
179
215
  callActive,
180
216
  children,
181
217
  }) => {
182
- const [isSTTError, setIsSTTError] = React.useState<boolean>(false);
183
- const [isCaptionON, setIsCaptionON] = React.useState<boolean>(false);
184
- const [isSTTActive, setIsSTTActive] = React.useState<boolean>(false);
185
- // const [language, setLanguage] = React.useState<[LanguageType]>(['']);
218
+ const {
219
+ data: {isHost, roomId},
220
+ } = useRoomInfo();
221
+ // Toast message
222
+ const heading = useString<'Set' | 'Changed'>(sttSpokenLanguageToastHeading);
223
+ const subheading = useString<sttSpokenLanguageToastSubHeadingDataInterface>(
224
+ sttSpokenLanguageToastSubHeading,
225
+ );
186
226
 
187
- // STT Form state - contains agentId, source, and target languages
188
- const [translationConfig, setTranslationConfig] =
189
- React.useState<LanguageTranslationConfig>({
190
- source: [],
191
- targets: [],
192
- });
227
+ const [isCaptionON, setIsCaptionON] = React.useState<boolean>(false);
193
228
 
194
229
  const [captionViewMode, setCaptionViewMode] =
195
- React.useState<CaptionViewMode>('translated');
196
-
230
+ React.useState<STTViewMode>('translated');
197
231
  const [transcriptViewMode, setTranscriptViewMode] =
198
- React.useState<CaptionViewMode>('translated');
232
+ React.useState<STTViewMode>('original-and-translated');
199
233
 
234
+ const [isSTTError, setIsSTTError] = React.useState<boolean>(false);
200
235
  const [isLangChangeInProgress, setIsLangChangeInProgress] =
201
236
  React.useState<boolean>(false);
202
- const [isTranslationChangeInProgress, setIsTranslationChangeInProgress] =
203
- React.useState<boolean>(false);
237
+
238
+ const [captionObj, setCaptionObj] = React.useState<CaptionObj>({});
204
239
  const [meetingTranscript, setMeetingTranscript] = React.useState<
205
240
  TranscriptItem[]
206
241
  >([]);
207
- const [captionObj, setCaptionObj] = React.useState<CaptionObj>({});
208
- console.log('[STT_PER_USER_BOT] captionObj: ', captionObj);
242
+
209
243
  const [isSTTListenerAdded, setIsSTTListenerAdded] =
210
244
  React.useState<boolean>(false);
211
245
  const [activeSpeakerUID, setActiveSpeakerUID] = React.useState<string>('');
212
246
  const [prevActiveSpeakerUID, setPrevActiveSpeakerUID] =
213
247
  React.useState<string>('');
214
- const [selectedTranslationLanguage, setSelectedTranslationLanguage] =
215
- React.useState<string>('');
216
248
  const [remoteSpokenLanguages, setRemoteSpokenLanguages] = React.useState<
217
249
  Record<string, LanguageType>
218
250
  >({});
219
251
 
252
+ // Default content
253
+ const {defaultContent} = useContent();
254
+ const defaultContentRef = React.useRef(defaultContent);
255
+ React.useEffect(() => {
256
+ defaultContentRef.current = defaultContent;
257
+ }, [defaultContent]);
258
+
259
+ // Active/prev speaker tracking (exposed as refs)
220
260
  const activeSpeakerRef = React.useRef('');
221
261
  const prevSpeakerRef = React.useRef('');
222
- const selectedTranslationLanguageRef = React.useRef('');
223
- const translationConfigRef = React.useRef<LanguageTranslationConfig>({
224
- source: [],
225
- targets: [],
262
+
263
+ // Global STT shared state
264
+ const [globalSttState, setGlobalSttState] = React.useState<GlobalSttState>({
265
+ globalSttEnabled: false,
266
+ globalSpokenLanguage: '',
267
+ globalTranslationTargets: [],
268
+ initiatorName: '',
226
269
  });
270
+ const globalSttStateRef = React.useRef(globalSttState);
271
+ React.useEffect(() => {
272
+ globalSttStateRef.current = globalSttState;
273
+ }, [globalSttState]);
227
274
 
228
- // Sync ref with state for selectedTranslationLanguage
275
+ // Queue for all stt operations
276
+ const sttEventQueueRef = React.useRef<SttQueueItem[]>([]);
277
+ const isProcessingSttEventRef = React.useRef(false);
278
+ const hasFlushedSttQueueRef = React.useRef(false);
279
+
280
+ // Selected Translated language
281
+ const [selectedTranslationLanguage, setSelectedTranslationLanguage] =
282
+ React.useState<LanguageType | null>(null);
283
+ const selectedTranslationLanguageRef = React.useRef<LanguageType | null>(
284
+ null,
285
+ );
229
286
  React.useEffect(() => {
230
287
  selectedTranslationLanguageRef.current = selectedTranslationLanguage;
231
288
  }, [selectedTranslationLanguage]);
232
289
 
233
- // Sync ref with state for translationConfig
234
- React.useEffect(() => {
235
- translationConfigRef.current = translationConfig;
236
- }, [translationConfig]);
290
+ const isSTTActive = globalSttState.globalSttEnabled;
237
291
 
238
- // Import STT API methods
292
+ // STT API methods
239
293
  const {start, stop, update} = useSTTAPI();
240
294
 
241
295
  const localUid = useLocalUid();
242
296
  const username = useGetName();
243
297
  const {hasUserJoinedRTM} = useContext(chatContext);
244
298
 
299
+ // Bot UID for this user
245
300
  const [localBotUid, setLocalBotUid] = React.useState<number | null>(null);
301
+ const localBotUidRef = React.useRef<number | null>(null);
302
+ React.useEffect(() => {
303
+ localBotUidRef.current = localBotUid;
304
+ }, [localBotUid]);
305
+
306
+ // Host flag
307
+ const isHostRef = React.useRef(isHost);
308
+ React.useEffect(() => {
309
+ isHostRef.current = isHost;
310
+ }, [isHost]);
246
311
 
247
312
  // i18n labels for error toasts
248
313
  const startErrorLabel = useString(sttStartError)();
249
314
  const updateErrorLabel = useString(sttUpdateError)();
250
315
 
251
- React.useEffect(() => {
252
- if (!localUid || !username || !hasUserJoinedRTM) {
253
- return;
254
- } // wait for room info to be ready
255
- const uid = generateBotUidForUser(localUid);
256
- setLocalBotUid(uid);
257
- }, [localUid, username, hasUserJoinedRTM]);
258
-
259
- // Silent update function to update local user's STT config without showing progress bar
260
- const silentUpdateSTT = React.useCallback(
261
- async (newtargetLanguages: LanguageType[]) => {
262
- try {
263
- // Merge new target languages with existing ones and keep unique
264
- const currentTargets = translationConfigRef.current?.targets || [];
265
- const mergedTargets = Array.from(
266
- new Set([...newtargetLanguages, ...currentTargets]),
267
- );
268
-
269
- const newConfig: LanguageTranslationConfig = {
270
- source: translationConfigRef.current?.source || [],
271
- targets: mergedTargets,
272
- };
273
-
274
- console.log(
275
- '[STT_PER_USER_BOT] Silent updating with merged targets:',
276
- 'newRemoteLanguages:',
277
- newtargetLanguages,
278
- 'existingTargets:',
279
- currentTargets,
280
- 'mergedTargets:',
281
- mergedTargets,
282
- );
283
-
284
- // Call update API without showing progress bar (don't set isLangChangeInProgress)
285
- const result = await update(localBotUid, newConfig);
316
+ // --- Derived readiness flag for STT ---
317
+ const sttDepsReady =
318
+ !!localUid &&
319
+ !!localBotUid &&
320
+ !!hasUserJoinedRTM &&
321
+ !!callActive &&
322
+ !!(roomId?.host || roomId?.attendee);
286
323
 
287
- if (result.success) {
288
- setTranslationConfig(newConfig);
289
- setIsSTTError(false);
290
-
291
- logger.log(
292
- LogSource.NetworkRest,
293
- 'stt',
294
- 'Local user STT updated silently',
295
- {newtargetLanguages, mergedTargets, botUid: localBotUid},
296
- );
297
- } else {
298
- setIsSTTError(true);
299
- logger.error(
300
- LogSource.NetworkRest,
301
- 'stt',
302
- 'Failed to silently update local user STT',
303
- result.error,
304
- );
305
- }
306
- } catch (error) {
307
- setIsSTTError(true);
308
- logger.error(
309
- LogSource.NetworkRest,
310
- 'stt',
311
- 'Error in silentUpdateSTT',
312
- error,
313
- );
314
- }
315
- },
316
- [localBotUid],
317
- );
324
+ const sttStartGuardRef = React.useRef(false);
325
+ const sttAutoStartGuardRef = React.useRef(false);
318
326
 
327
+ // STT dependencues
328
+ const sttDepsReadyRef = React.useRef(false);
319
329
  React.useEffect(() => {
320
- const remoteLangs = Array.from(
321
- new Set(
322
- Object.entries(remoteSpokenLanguages)
323
- .filter(([uid, lang]) => uid !== String(localUid) && lang)
324
- .map(([, lang]) => lang),
325
- ),
326
- );
327
-
328
- // If STT is active, check if received language differs from current target languages
329
- if (isSTTActive && remoteLangs && remoteLangs.length > 0) {
330
- const currentTargetLanguages =
331
- translationConfigRef.current?.targets || [];
332
- // Only update if any received language is not in current target languages
333
- const hasTargetsChanged = remoteLangs.some(
334
- lang => !currentTargetLanguages.includes(lang),
335
- );
336
- if (hasTargetsChanged) {
337
- console.log(
338
- '[STT_PER_USER_BOT] Spoken language change detected',
339
- 'currentTargets:',
340
- currentTargetLanguages,
341
- 'receivedSpokenLanguages (unique):',
342
- remoteLangs,
343
- );
344
- // Call silentUpdateSTT directly
345
- silentUpdateSTT(remoteLangs);
346
- }
347
- }
348
- }, [remoteSpokenLanguages]);
330
+ sttDepsReadyRef.current = sttDepsReady;
331
+ }, [sttDepsReady]);
349
332
 
350
- // Listen for spoken language updates from other users
351
333
  React.useEffect(() => {
352
- const handleSpokenLanguage = (data: any) => {
353
- try {
354
- const payload = JSON.parse(data.payload);
355
- const {userUid, spokenLanguage, username} = payload;
356
-
357
- console.log(
358
- '[STT_PER_USER_BOT] Received spoken language from user:',
359
- username,
360
- 'userUid:',
361
- userUid,
362
- 'spokenLanguage:',
363
- spokenLanguage,
364
- );
365
-
366
- // Update remoteSpokenLanguages with the user's spoken language
367
- setRemoteSpokenLanguages(prev => ({
368
- ...prev,
369
- [userUid]: spokenLanguage,
370
- }));
371
- } catch (error) {
372
- logger.error(
373
- LogSource.Internals,
374
- 'STT',
375
- 'Failed to parse STT_SPOKEN_LANGUAGE event',
376
- error,
377
- );
378
- }
379
- };
380
-
381
- events.on(EventNames.STT_SPOKEN_LANGUAGE, handleSpokenLanguage);
334
+ if (sttDepsReadyRef.current && !hasFlushedSttQueueRef.current) {
335
+ hasFlushedSttQueueRef.current = true;
336
+ processSttEventQueue();
337
+ }
338
+ // When deps become ready → flush queue once
339
+ }, [sttDepsReady]);
382
340
 
383
- // Cleanup listener on unmount
384
- return () => {
385
- events.off(EventNames.STT_SPOKEN_LANGUAGE, handleSpokenLanguage);
386
- };
341
+ // Helper: convert user UID -> bot UID
342
+ const generateBotUidForUser = React.useCallback((userLocalUid: number) => {
343
+ return 900000000 + (userLocalUid % 100000000);
387
344
  }, []);
388
345
 
389
- // Listen for when remote users stop translation and clear their translations
346
+ // Generate bot UID once deps are ready enough
390
347
  React.useEffect(() => {
391
- const handleUserStoppedTranslation = (data: any) => {
392
- try {
393
- const payload = JSON.parse(data.payload);
394
- const {userUid, username} = payload;
395
-
396
- console.log(
397
- '[STT_PER_USER_BOT] User stopped translation:',
398
- username,
399
- 'userUid:',
400
- userUid,
401
- );
402
-
403
- // Clear translations for this user by setting captionObj translations to empty
404
- setCaptionObj(prevState => {
405
- if (prevState[userUid]) {
406
- return {
407
- ...prevState,
408
- [userUid]: {
409
- ...prevState[userUid],
410
- translations: [], // Clear translations
411
- },
412
- };
413
- }
414
- return prevState;
415
- });
416
- } catch (error) {
417
- logger.error(
418
- LogSource.Internals,
419
- 'STT',
420
- 'Failed to parse USER_STOPPED_TRANSLATION event',
421
- error,
422
- );
423
- }
424
- };
348
+ if (!localUid || !username || !hasUserJoinedRTM) {
349
+ return;
350
+ } // wait for room info to be ready
351
+ const uid = generateBotUidForUser(localUid);
352
+ setLocalBotUid(uid);
353
+ }, [localUid, username, generateBotUidForUser, hasUserJoinedRTM]);
425
354
 
426
- events.on(
427
- EventNames.USER_STOPPED_TRANSLATION,
428
- handleUserStoppedTranslation,
429
- );
355
+ const buildSttTranscriptForSourceChanged = (
356
+ prevSpokenLang: LanguageType,
357
+ newSpokenLang: LanguageType,
358
+ ) => {
359
+ const spokenLanguageChanged = prevSpokenLang !== newSpokenLang;
360
+ if (!spokenLanguageChanged) {
361
+ return null;
362
+ }
363
+ let message = '';
364
+ // Spoken lang changed
365
+ if (!prevSpokenLang) {
366
+ // First time STT is enabled
367
+ message = `Spoken language set to "${getLanguageLabel([newSpokenLang])}"`;
368
+ } else {
369
+ message = `Spoken language changed from "${getLanguageLabel([
370
+ prevSpokenLang,
371
+ ])}" to "${getLanguageLabel([newSpokenLang])}"`;
372
+ }
373
+ setMeetingTranscript(prev => [
374
+ ...prev,
375
+ {
376
+ name: 'langUpdate',
377
+ time: new Date().getTime(),
378
+ uid: `langUpdate-${localUid}`,
379
+ text: message,
380
+ },
381
+ ]);
382
+ };
430
383
 
431
- // Cleanup listener on unmount
432
- return () => {
433
- events.off(
434
- EventNames.USER_STOPPED_TRANSLATION,
435
- handleUserStoppedTranslation,
436
- );
437
- };
438
- }, []);
384
+ const buildSttTranscriptForTargetChanged = (
385
+ prevSelectedTargetLang: LanguageType | null,
386
+ newSelectedTargetLang: LanguageType | null,
387
+ reason?: TargetChange['reason'],
388
+ ) => {
389
+ const targetLanguageChanged =
390
+ prevSelectedTargetLang !== newSelectedTargetLang;
391
+ if (!targetLanguageChanged) {
392
+ return null;
393
+ }
394
+ let message = '';
395
+
396
+ if (reason === 'spoken-language-changed') {
397
+ message = `Translation for "${getLanguageLabel([
398
+ prevSelectedTargetLang,
399
+ ])}" was turned off because the spoken language changed to ${getLanguageLabel(
400
+ [prevSelectedTargetLang],
401
+ )}`;
402
+ }
403
+ // Target lang changed
404
+ // Case 1: User turned translation OFF
405
+ else if (prevSelectedTargetLang && !newSelectedTargetLang) {
406
+ message = 'Translation turned off';
407
+ }
408
+ // Case 2: User selected ANY new translation
409
+ else {
410
+ message = `Translation set to "${getLanguageLabel([
411
+ newSelectedTargetLang,
412
+ ])}"`;
413
+ }
414
+ setMeetingTranscript(prev => [
415
+ ...prev,
416
+ {
417
+ name: 'translationUpdate',
418
+ uid: `translationUpdate-${localUid}`,
419
+ time: new Date().getTime(),
420
+ text: message,
421
+ selectedTranslationLanguage: newSelectedTargetLang,
422
+ },
423
+ ]);
424
+ };
439
425
 
440
426
  const startSTTBotSession = async (
441
427
  newConfig: LanguageTranslationConfig,
442
428
  ): Promise<STTAPIResponse> => {
443
- if (!localBotUid || !localUid) {
444
- console.warn('[STT] Missing localUid or botUid');
445
- return {
446
- success: false,
447
- error: {message: 'Missing localUid or botUid'},
448
- };
449
- }
450
-
451
429
  try {
452
430
  setIsLangChangeInProgress(true);
453
- const result = await start(localBotUid, newConfig);
454
- console.log('STT start result: ', result);
455
-
431
+ const result = await start(localBotUidRef.current, newConfig);
432
+ console.log('[STT] start result: ', result);
456
433
  if (result.success || result.error?.code === 610) {
457
434
  // Success or already started
458
- setIsSTTActive(true);
459
435
  setIsSTTError(false);
460
-
461
- // Add transcript entry for language change
462
- // If STT was not active before, this is the first time setting the language
463
- const actionText = !isSTTActive
464
- ? `has set the spoken language to "${getLanguageLabel(
465
- newConfig.source,
466
- )}"`
467
- : `changed the spoken language from "${getLanguageLabel(
468
- translationConfig?.source,
469
- )}" to "${getLanguageLabel(newConfig.source)}"`;
470
-
471
- setTranslationConfig(newConfig);
472
- setMeetingTranscript(prev => [
473
- ...prev,
474
- {
475
- name: 'langUpdate',
476
- time: new Date().getTime(),
477
- uid: `langUpdate-${localUid}`,
478
- text: actionText,
479
- },
480
- ]);
481
-
482
- // Broadcast spoken language to all users
483
- events.send(
484
- EventNames.STT_SPOKEN_LANGUAGE,
485
- JSON.stringify({
486
- userUid: localUid,
487
- spokenLanguage: newConfig.source[0],
488
- username: username,
489
- }),
490
- PersistanceLevel.Sender,
491
- );
492
-
493
436
  logger.log(
494
437
  LogSource.NetworkRest,
495
438
  'stt',
@@ -497,7 +440,6 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
497
440
  result.data,
498
441
  );
499
442
  } else {
500
- // setIsCaptionON(false);
501
443
  setIsSTTError(true);
502
444
  logger.error(
503
445
  LogSource.NetworkRest,
@@ -505,18 +447,19 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
505
447
  'Failed to start STT',
506
448
  result.error,
507
449
  );
508
- // Show error toast: text1 = translated label, text2 = API error
509
450
  Toast.show({
510
451
  leadingIconName: 'alert',
511
452
  type: 'error',
512
453
  text1: startErrorLabel,
513
454
  text2: result.error?.message || 'Unknown error occurred',
514
455
  visibilityTime: 4000,
456
+ primaryBtn: null,
457
+ secondaryBtn: null,
515
458
  });
516
459
  }
517
460
  setIsLangChangeInProgress(false);
518
461
  return result;
519
- } catch (error) {
462
+ } catch (error: any) {
520
463
  setIsLangChangeInProgress(false);
521
464
  setIsSTTError(true);
522
465
  logger.error(LogSource.NetworkRest, 'stt', 'STT start error', error);
@@ -527,6 +470,8 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
527
470
  text1: startErrorLabel,
528
471
  text2: error?.message || 'Unknown error occurred',
529
472
  visibilityTime: 4000,
473
+ primaryBtn: null,
474
+ secondaryBtn: null,
530
475
  });
531
476
  return {
532
477
  success: false,
@@ -537,106 +482,25 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
537
482
 
538
483
  const updateSTTBotSession = async (
539
484
  newConfig: LanguageTranslationConfig,
485
+ isLocal = false,
486
+ targetChange?: TargetChange,
540
487
  ): Promise<STTAPIResponse> => {
541
- if (!localBotUid || !localUid) {
542
- console.warn('[STT] Missing localUid or botUid');
543
- return {
544
- success: false,
545
- error: {message: 'Missing localUid or botUid'},
546
- };
547
- }
548
-
549
488
  try {
550
- setIsLangChangeInProgress(true);
551
- const result = await update(localBotUid, newConfig);
552
-
489
+ isLocal && setIsLangChangeInProgress(true);
490
+ const result = await update(localBotUidRef.current, newConfig);
553
491
  if (result.success) {
554
- setTranslationConfig(newConfig);
555
492
  setIsSTTError(false);
556
493
 
557
- // Add transcript entry for language change
558
- // Determine what actually changed
559
- const spokenLanguageChanged =
560
- translationConfig?.source[0] !== newConfig.source[0];
561
- const oldTargetsSorted = (translationConfig?.targets || [])
562
- .sort()
563
- .map(lang => getLanguageLabel([lang]))
564
- .join(', ');
565
- const newTargetsSorted = (newConfig.targets || [])
566
- .sort()
567
- .map(lang => getLanguageLabel([lang]))
568
- .join(', ');
569
- const targetsChanged = oldTargetsSorted !== newTargetsSorted;
570
-
571
- const oldTargetsLength = translationConfig?.targets?.length || 0;
572
- const newTargetsLength = newConfig.targets?.length || 0;
573
- const targetsWereDisabled =
574
- oldTargetsLength > 0 && newTargetsLength === 0;
575
- const targetsWereEnabled =
576
- oldTargetsLength === 0 && newTargetsLength > 0;
577
-
578
- // Build target message once
579
- let targetMessage = '';
580
- if (targetsChanged) {
581
- if (targetsWereDisabled) {
582
- targetMessage = 'stopped translations';
583
- } else if (targetsWereEnabled) {
584
- targetMessage = `enabled translations to "${newTargetsSorted}"`;
585
- } else {
586
- targetMessage = `changed target translation languages to "${newTargetsSorted}"`;
587
- }
588
- }
589
-
590
- // Build action text
591
- let actionText = '';
592
- if (spokenLanguageChanged && targetsChanged) {
593
- // Both spoken language and targets changed
594
- actionText = `changed spoken language from "${getLanguageLabel(
595
- translationConfig?.source,
596
- )}" to "${getLanguageLabel(newConfig.source)}" and ${targetMessage}`;
597
- } else if (spokenLanguageChanged) {
598
- // Only spoken language changed
599
- actionText = `changed the spoken language from "${getLanguageLabel(
600
- translationConfig?.source,
601
- )}" to "${getLanguageLabel(newConfig.source)}"`;
602
- } else if (targetsChanged) {
603
- // Only target languages changed
604
- actionText = targetMessage;
605
- }
606
-
607
- if (actionText) {
608
- setMeetingTranscript(prev => [
609
- ...prev,
610
- {
611
- name: 'langUpdate',
612
- uid: `langUpdate-${localUid}`,
613
- time: new Date().getTime(),
614
- text: actionText,
615
- },
616
- ]);
617
- }
618
-
619
- // Broadcast updated spoken language to all users (only if it changed)
620
- if (spokenLanguageChanged) {
621
- events.send(
622
- EventNames.STT_SPOKEN_LANGUAGE,
623
- JSON.stringify({
624
- userUid: localUid,
625
- spokenLanguage: newConfig.source[0],
626
- username: username,
627
- }),
628
- PersistanceLevel.Sender,
629
- );
630
- }
494
+ const oldSource = globalSttStateRef.current.globalSpokenLanguage;
495
+ const newSource = newConfig.source[0];
631
496
 
632
- // Send event when user stops translation
633
- if (targetsWereDisabled) {
634
- events.send(
635
- EventNames.USER_STOPPED_TRANSLATION,
636
- JSON.stringify({
637
- userUid: localUid,
638
- username: username,
639
- }),
497
+ // Build transcript messages if source changed
498
+ buildSttTranscriptForSourceChanged(oldSource, newSource);
499
+ if (isLocal && targetChange) {
500
+ buildSttTranscriptForTargetChanged(
501
+ targetChange?.prev,
502
+ targetChange?.next,
503
+ targetChange?.reason,
640
504
  );
641
505
  }
642
506
 
@@ -661,11 +525,13 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
661
525
  text1: updateErrorLabel,
662
526
  text2: result.error?.message || 'Unknown error occurred',
663
527
  visibilityTime: 4000,
528
+ primaryBtn: null,
529
+ secondaryBtn: null,
664
530
  });
665
531
  }
666
532
  setIsLangChangeInProgress(false);
667
533
  return result;
668
- } catch (error) {
534
+ } catch (error: any) {
669
535
  setIsLangChangeInProgress(false);
670
536
  setIsSTTError(true);
671
537
  logger.error(LogSource.NetworkRest, 'stt', 'STT update error', error);
@@ -676,6 +542,8 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
676
542
  text1: updateErrorLabel,
677
543
  text2: error?.message || 'Unknown error occurred',
678
544
  visibilityTime: 4000,
545
+ primaryBtn: null,
546
+ secondaryBtn: null,
679
547
  });
680
548
  return {
681
549
  success: false,
@@ -684,88 +552,20 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
684
552
  }
685
553
  };
686
554
 
687
- const handleTranslateConfigChange = async (
688
- inputTranslateConfig: LanguageTranslationConfig,
689
- ) => {
690
- if (!localBotUid || !localUid) {
691
- console.warn('[STT] Missing localUid or botUid');
692
- return;
693
- }
694
-
695
- const newConfig: LanguageTranslationConfig = {
696
- source: inputTranslateConfig?.source,
697
- targets: inputTranslateConfig?.targets,
698
- };
699
-
700
- let action: 'start' | 'update' = 'start';
701
- if (!isSTTActive) {
702
- action = 'start';
703
- } else if (hasConfigChanged(translationConfig, newConfig)) {
704
- action = 'update';
705
- }
706
-
707
- console.log('[STT_HANDLE_CONFIRM]', {
708
- action,
709
- localBotUid,
710
- inputConfig: inputTranslateConfig,
711
- sanitizedConfig: newConfig,
712
- });
713
-
714
- try {
715
- switch (action) {
716
- case 'start':
717
- const startResult = await startSTTBotSession(newConfig);
718
- if (!startResult.success) {
719
- throw new Error(
720
- startResult.error?.message || 'Failed to start STT',
721
- );
722
- }
723
- break;
724
-
725
- case 'update':
726
- const updateResult = await updateSTTBotSession(newConfig);
727
- if (!updateResult.success) {
728
- throw new Error(
729
- updateResult.error?.message || 'Failed to update STT config',
730
- );
731
- }
732
- break;
733
-
734
- default:
735
- console.warn('Unknown STT action');
736
- }
737
- } catch (error) {
738
- setIsSTTError(true);
739
- setIsLangChangeInProgress(false);
740
- logger.error(
741
- LogSource.NetworkRest,
742
- 'stt',
743
- 'Error in handleTranslateConfigChange',
744
- error,
745
- );
746
- throw error;
747
- }
748
- };
749
-
750
555
  const stopSTTBotSession = React.useCallback(async () => {
751
- console.log('[STT_PER_USER_BOT] stopCaption called');
556
+ console.log('[STT] stopSTTBotSession called');
752
557
 
753
- if (!localBotUid) {
754
- console.warn('[STT] Missing botUid');
558
+ if (!localBotUidRef.current) {
559
+ console.warn('[STT] Missing botUid for stop');
755
560
  return;
756
561
  }
757
562
 
758
563
  try {
759
- const result = await stop(localBotUid);
564
+ const result = await stop(localBotUidRef.current);
760
565
 
761
566
  if (result.success) {
762
567
  // Set STT inactive locally
763
- setIsSTTActive(false);
764
568
  // Clear user's source language when stopping
765
- setTranslationConfig({
766
- source: [],
767
- targets: [],
768
- });
769
569
  // setIsCaptionON(false);
770
570
  setIsSTTError(false);
771
571
 
@@ -792,14 +592,8 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
792
592
  'Error in stopSTTBotSession',
793
593
  error,
794
594
  );
795
- throw error;
796
595
  }
797
- }, [stop, localBotUid]);
798
-
799
- // Helper function to convert user UID to stt bot UID
800
- const generateBotUidForUser = (userLocalUid: number): number => {
801
- return 900000000 + (userLocalUid % 100000000);
802
- };
596
+ }, [stop]);
803
597
 
804
598
  // Helper function to convert bot UID to user UID
805
599
  // Bot UIDs are in format: 900000000 + userUid
@@ -825,6 +619,379 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
825
619
  [],
826
620
  );
827
621
 
622
+ // Spoken language handler
623
+ const confirmSpokenLanguageChange = React.useCallback(
624
+ async (newSpokenLang: LanguageType) => {
625
+ try {
626
+ const prevState = globalSttStateRef.current;
627
+ const prevTargets = prevState.globalTranslationTargets || [];
628
+ const prevSelectedTarget = selectedTranslationLanguageRef.current;
629
+
630
+ // Remove spoken from targets
631
+ const cleanedTargets = prevTargets.filter(t => t !== newSpokenLang);
632
+
633
+ // Build update state
634
+ const updatedState: GlobalSttState = {
635
+ ...prevState,
636
+ globalSttEnabled: true,
637
+ globalSpokenLanguage: newSpokenLang,
638
+ globalTranslationTargets: cleanedTargets,
639
+ initiatorName: defaultContentRef?.current[localUid]?.name || username,
640
+ };
641
+
642
+ // Check if selected target still exists in target
643
+ const isSelectedStillValid =
644
+ prevSelectedTarget && cleanedTargets.includes(prevSelectedTarget);
645
+
646
+ // Prepare the target change
647
+ let targetChange: TargetChange;
648
+ if (prevSelectedTarget) {
649
+ if (isSelectedStillValid) {
650
+ // target remains valid
651
+ targetChange = {prev: prevSelectedTarget, next: prevSelectedTarget};
652
+ } else {
653
+ // target becomes invalid → must be turned OFF
654
+ targetChange = {
655
+ prev: prevSelectedTarget,
656
+ next: null,
657
+ reason: 'spoken-language-changed',
658
+ };
659
+ }
660
+ }
661
+ // Queue
662
+ enqueueSttEvent(updatedState, true, targetChange);
663
+
664
+ console.log(
665
+ '[STT_GLOBAL] confirmSpokenLanguageChange sent STT_GLOBAL_STATE: ',
666
+ updatedState,
667
+ );
668
+ } catch (error) {
669
+ console.log('[STT_GLOBAL] confirmSpokenLanguageChange error: ', error);
670
+ }
671
+ },
672
+ [localUid], // only real dependency
673
+ );
674
+
675
+ // Target language handler
676
+ const confirmTargetLanguageChange = React.useCallback(
677
+ async (newTargetLang: LanguageType) => {
678
+ // 1. User selected "Off"
679
+ const prevSelectedTargetLang = selectedTranslationLanguage;
680
+ if (!newTargetLang) {
681
+ buildSttTranscriptForTargetChanged(
682
+ prevSelectedTargetLang,
683
+ newTargetLang,
684
+ );
685
+ setSelectedTranslationLanguage(null);
686
+ return;
687
+ }
688
+ // 2. User selected a translation language.
689
+ // 3. Check if target is already included in global targets
690
+ const alreadyInGlobal =
691
+ globalSttStateRef.current.globalTranslationTargets.includes(
692
+ newTargetLang,
693
+ );
694
+ if (alreadyInGlobal) {
695
+ buildSttTranscriptForTargetChanged(
696
+ prevSelectedTargetLang,
697
+ newTargetLang,
698
+ );
699
+ setSelectedTranslationLanguage(newTargetLang);
700
+ return;
701
+ }
702
+ // 4. Not in global targets, need to create updated state to pass to api
703
+ const prevTargets = globalSttStateRef.current.globalTranslationTargets;
704
+ const newTargets = Array.from(new Set([...prevTargets, newTargetLang]));
705
+
706
+ const updatedState: GlobalSttState = {
707
+ globalSttEnabled: globalSttStateRef.current.globalSttEnabled,
708
+ globalSpokenLanguage: globalSttStateRef.current.globalSpokenLanguage,
709
+ globalTranslationTargets: newTargets,
710
+ };
711
+ // 5. Queue event
712
+ const prevTargetLang = selectedTranslationLanguageRef.current;
713
+ enqueueSttEvent(updatedState, true, {
714
+ prev: prevTargetLang,
715
+ next: newTargetLang,
716
+ });
717
+ },
718
+ [selectedTranslationLanguage], // only real state dependency
719
+ );
720
+
721
+ // Queues all local + remote stt events
722
+ const enqueueSttEvent = React.useCallback(
723
+ (state: GlobalSttState, isLocal: boolean, targetChange?: TargetChange) => {
724
+ console.log(
725
+ '[STT_GLOBAL] inside enqueueSttEvent - sttDepsReadyRef flag',
726
+ sttDepsReadyRef.current,
727
+ );
728
+ sttEventQueueRef.current.push({
729
+ state,
730
+ isLocal,
731
+ targetChange,
732
+ });
733
+ if (sttDepsReadyRef.current) {
734
+ processSttEventQueue();
735
+ }
736
+ },
737
+ [],
738
+ );
739
+
740
+ // Process stt events
741
+ const processSttEventQueue = React.useCallback(async () => {
742
+ // 1. Concurrent queue processing
743
+ if (isProcessingSttEventRef.current) {
744
+ return;
745
+ }
746
+ isProcessingSttEventRef.current = true;
747
+ // 2. stt auto start check
748
+ if (
749
+ $config.STT_AUTO_START &&
750
+ sttDepsReadyRef.current &&
751
+ !sttAutoStartGuardRef.current
752
+ ) {
753
+ if (isHostRef.current && !globalSttStateRef.current.globalSttEnabled) {
754
+ console.log('[STT] AUTO_START →supriya injecting start state');
755
+
756
+ sttAutoStartGuardRef.current = true;
757
+
758
+ const autoStartState: GlobalSttState = {
759
+ globalSttEnabled: true,
760
+ globalSpokenLanguage: 'en-US',
761
+ globalTranslationTargets: [],
762
+ initiatorName: defaultContentRef.current[localUid]?.name || username,
763
+ };
764
+
765
+ // adding auto start in the beginning of queue
766
+ sttEventQueueRef.current.unshift({
767
+ state: autoStartState,
768
+ isLocal: true,
769
+ });
770
+ }
771
+ }
772
+ // 3. queue processing of all stt events
773
+ while (sttEventQueueRef.current.length > 0) {
774
+ const item = sttEventQueueRef.current.shift();
775
+ if (!item) {
776
+ break;
777
+ }
778
+ const {state: newState, isLocal, targetChange} = item;
779
+ const prevState = globalSttStateRef.current;
780
+ if (isSameState(prevState, newState)) {
781
+ console.log('[STT] Skipped duplicate STT_GLOBAL_STATE');
782
+ continue; // no call to processGlobalSttSingleEvent
783
+ }
784
+ const ok = await processGlobalSttSingleEvent(
785
+ newState,
786
+ isLocal,
787
+ targetChange,
788
+ );
789
+ if (!ok) {
790
+ console.warn('[STT] Skipping global state update because API failed.');
791
+ continue;
792
+ }
793
+ // update global state AFTER processing
794
+ setGlobalSttState(newState);
795
+ globalSttStateRef.current = newState;
796
+ }
797
+ isProcessingSttEventRef.current = false;
798
+ }, []);
799
+
800
+ const processGlobalSttSingleEvent = React.useCallback(
801
+ async (
802
+ newState: GlobalSttState,
803
+ isLocal: boolean,
804
+ targetChange?: TargetChange,
805
+ ) => {
806
+ const prevState = globalSttStateRef.current;
807
+ const wasEnabledBefore = prevState.globalSttEnabled;
808
+ const isEnabledNow = newState.globalSttEnabled;
809
+
810
+ const isStartOperation = !wasEnabledBefore && isEnabledNow;
811
+ const isUpdateOperation = wasEnabledBefore && isEnabledNow;
812
+ const isStopOperation = wasEnabledBefore && !isEnabledNow;
813
+ try {
814
+ if (isStartOperation) {
815
+ console.log('[STT] Remote global STT -> starting session', newState);
816
+ // Start guard starts
817
+ if (sttStartGuardRef.current) {
818
+ console.log('[STT] Start skipped (already started)');
819
+ return;
820
+ }
821
+ sttStartGuardRef.current = true;
822
+ // Start guard ends
823
+ const result = await startSTTBotSession({
824
+ source: [newState.globalSpokenLanguage],
825
+ targets: newState.globalTranslationTargets,
826
+ });
827
+ if (!result.success) {
828
+ return false;
829
+ }
830
+ buildSttTranscriptForSourceChanged(
831
+ prevState.globalSpokenLanguage,
832
+ newState.globalSpokenLanguage,
833
+ );
834
+ // Toast
835
+ let spokenLangLabel =
836
+ getLanguageLabel([newState?.globalSpokenLanguage]) || '';
837
+ if (isLocal) {
838
+ // I see this
839
+ if (sttAutoStartGuardRef.current) {
840
+ Toast.show({
841
+ type: 'info',
842
+ text1: heading('Set'),
843
+ text2: `Live transcription are automatically enabled for this meeting in "${spokenLangLabel}"`,
844
+ visibilityTime: 3000,
845
+ primaryBtn: null,
846
+ secondaryBtn: null,
847
+ });
848
+ } else {
849
+ Toast.show({
850
+ type: 'info',
851
+ text1: heading('Set'),
852
+ text2: subheading({
853
+ username: 'You',
854
+ action: 'Set',
855
+ newLanguage: spokenLangLabel,
856
+ }),
857
+ visibilityTime: 3000,
858
+ primaryBtn: null,
859
+ secondaryBtn: null,
860
+ });
861
+ }
862
+ } else {
863
+ // Remote users see this
864
+ let subheadingObj: sttSpokenLanguageToastSubHeadingDataInterface = {
865
+ username: newState?.initiatorName || 'Host',
866
+ action: 'Set',
867
+ newLanguage: getLanguageLabel([newState?.globalSpokenLanguage]),
868
+ };
869
+ Toast.show({
870
+ type: 'info',
871
+ text1: heading('Set'),
872
+ text2: subheading(subheadingObj),
873
+ visibilityTime: 3000,
874
+ primaryBtn: null,
875
+ secondaryBtn: null,
876
+ });
877
+ }
878
+
879
+ if (isLocal) {
880
+ events.send(
881
+ EventNames.STT_GLOBAL_STATE,
882
+ JSON.stringify(newState),
883
+ PersistanceLevel.Session,
884
+ );
885
+ }
886
+ return true;
887
+ } else if (isUpdateOperation) {
888
+ console.log('[STT] Global STT -> updating session', newState);
889
+ const result = await updateSTTBotSession(
890
+ {
891
+ source: [newState.globalSpokenLanguage],
892
+ targets: newState.globalTranslationTargets,
893
+ },
894
+ isLocal,
895
+ targetChange ?? undefined,
896
+ );
897
+ if (!result.success) {
898
+ return false;
899
+ }
900
+ if (
901
+ prevState.globalSpokenLanguage !== newState.globalSpokenLanguage
902
+ ) {
903
+ let subheadingObj: sttSpokenLanguageToastSubHeadingDataInterface = {
904
+ username: isLocal ? 'You' : newState.initiatorName || 'Host',
905
+ action: 'Changed',
906
+ newLanguage:
907
+ getLanguageLabel([newState.globalSpokenLanguage]) || '',
908
+ oldLanguage:
909
+ getLanguageLabel([prevState.globalSpokenLanguage]) || '',
910
+ };
911
+ // text1: 'Spoken language updated',
912
+ // text2: `Captions will now transcribe in ${getLanguageLabel(
913
+ // newConfig.source,
914
+ // )}`,
915
+ Toast.show({
916
+ type: 'info',
917
+ text1: heading('Changed'),
918
+ text2: subheading(subheadingObj),
919
+ // text2: `${subheading(
920
+ // subheadingObj,
921
+ // )} \n Captions will now transcribe in ${
922
+ // getLanguageLabel(newConfig.source) || ''
923
+ // }`,
924
+ // text1: 'Spoken language updated',
925
+ // text2: `Captions will now transcribe in ${getLanguageLabel(
926
+ // newConfig.source,
927
+ // )}`,
928
+ visibilityTime: 3000,
929
+ primaryBtn: null,
930
+ secondaryBtn: null,
931
+ });
932
+ }
933
+ if (isLocal) {
934
+ events.send(
935
+ EventNames.STT_GLOBAL_STATE,
936
+ JSON.stringify(newState),
937
+ PersistanceLevel.Session,
938
+ );
939
+ setSelectedTranslationLanguage(targetChange?.next);
940
+ } else {
941
+ const currentSelectedTarget =
942
+ selectedTranslationLanguageRef.current;
943
+ if (
944
+ currentSelectedTarget &&
945
+ !newState.globalTranslationTargets.includes(currentSelectedTarget)
946
+ ) {
947
+ setSelectedTranslationLanguage(null);
948
+ }
949
+ }
950
+ return true;
951
+ } else if (isStopOperation) {
952
+ console.log('[STT] Global STT -> stopping session', newState);
953
+ await stopSTTBotSession();
954
+ sttStartGuardRef.current = false;
955
+ return true;
956
+ }
957
+ return true;
958
+ } catch (error) {
959
+ logger.error(
960
+ LogSource.Internals,
961
+ 'STT',
962
+ 'Error handling STT_GLOBAL_STATE event',
963
+ error,
964
+ );
965
+ return false;
966
+ }
967
+ },
968
+ [],
969
+ );
970
+
971
+ // Handle GLOBAL STT events from others (simple "machine" + queue)
972
+ React.useEffect(() => {
973
+ const handleGlobalSTTChange = async (evt: any) => {
974
+ const {payload} = evt || {};
975
+ console.log('[STT] STT_GLOBAL_STATE event received: ', evt);
976
+ let newState: GlobalSttState;
977
+ try {
978
+ newState = JSON.parse(payload);
979
+ } catch (error) {
980
+ logger.error(
981
+ LogSource.Internals,
982
+ 'STT',
983
+ 'Failed to parse STT_GLOBAL_STATE event payload',
984
+ error,
985
+ );
986
+ return;
987
+ }
988
+ enqueueSttEvent(newState, false);
989
+ };
990
+
991
+ events.on(EventNames.STT_GLOBAL_STATE, handleGlobalSTTChange);
992
+ return () => events.off(EventNames.STT_GLOBAL_STATE, handleGlobalSTTChange);
993
+ }, []);
994
+
828
995
  return (
829
996
  <CaptionContext.Provider
830
997
  value={{
@@ -833,9 +1000,10 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
833
1000
  isSTTError,
834
1001
  setIsSTTError,
835
1002
  isSTTActive,
836
- setIsSTTActive,
837
- translationConfig,
838
- setTranslationConfig,
1003
+ sttDepsReady,
1004
+ globalSttState,
1005
+ confirmSpokenLanguageChange,
1006
+ confirmTargetLanguageChange,
839
1007
  captionViewMode,
840
1008
  setCaptionViewMode,
841
1009
  transcriptViewMode,
@@ -844,8 +1012,6 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
844
1012
  setMeetingTranscript,
845
1013
  isLangChangeInProgress,
846
1014
  setIsLangChangeInProgress,
847
- isTranslationChangeInProgress,
848
- setIsTranslationChangeInProgress,
849
1015
  captionObj,
850
1016
  setCaptionObj,
851
1017
  isSTTListenerAdded,
@@ -853,12 +1019,9 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
853
1019
  activeSpeakerRef,
854
1020
  prevSpeakerRef,
855
1021
  selectedTranslationLanguage,
856
- setSelectedTranslationLanguage,
857
1022
  selectedTranslationLanguageRef,
858
- translationConfigRef,
859
1023
  remoteSpokenLanguages,
860
1024
  setRemoteSpokenLanguages,
861
- handleTranslateConfigChange,
862
1025
  startSTTBotSession,
863
1026
  updateSTTBotSession,
864
1027
  stopSTTBotSession,