From 50f1f1977e87ccae612bc6b81e3b58362e097b06 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 17 Feb 2026 20:14:51 +0100 Subject: [PATCH] fix: captions enabling for authenticated Teams UI - add submenu path, reduce timeouts, add debug logging Co-authored-by: Cursor --- src/bot/captionsProcedure.ts | 247 +++++++++++++++++++++++++++-------- 1 file changed, 193 insertions(+), 54 deletions(-) diff --git a/src/bot/captionsProcedure.ts b/src/bot/captionsProcedure.ts index 20c58b9..5f60c6a 100644 --- a/src/bot/captionsProcedure.ts +++ b/src/bot/captionsProcedure.ts @@ -58,37 +58,24 @@ export class CaptionsProcedure { /** * Open the "More actions" (...) menu in the call controls. - * Primary selector: button[id="callingButtons-showMoreBtn"] (confirmed by Recall.ai). + * Works for both anonymous (light-meetings) and authenticated (full Teams) UI. */ private async _openMoreMenu(): Promise { - // Primary selector - confirmed by Recall.ai (Jan 2025) - const primarySelector = 'button[id="callingButtons-showMoreBtn"]'; - - try { - await this._page.waitForSelector(primarySelector, { timeout: 120000 }); - this._logger.info('Found "More" button'); - await this._page.click(primarySelector); - this._logger.info('Clicked "More" button'); - return; - } catch { - this._logger.info('Primary more button selector not found, trying fallbacks...'); - } - - // Fallback selectors - const fallbackSelectors = [ + const allSelectors = [ + 'button[id="callingButtons-showMoreBtn"]', '[data-tid="callingButtons-showMoreBtn"]', 'button[aria-label*="More actions"]', 'button[aria-label*="More"]', '[data-tid="more-button"]', ]; - for (const selector of fallbackSelectors) { + for (const selector of allSelectors) { try { const button = await this._page.$(selector); if (button) { await button.click(); + this._logger.info(`Clicked "More" button: ${selector}`); await this._page.waitForTimeout(1000); - this._logger.info(`Opened more menu (fallback: ${selector})`); return; } } catch { @@ -96,44 +83,44 @@ export class CaptionsProcedure { } } + // Last resort: wait for the primary selector with a short timeout + try { + await this._page.waitForSelector(allSelectors[0], { timeout: 10000 }); + await this._page.click(allSelectors[0]); + this._logger.info('Found "More" button (after wait)'); + await this._page.waitForTimeout(1000); + return; + } catch { + // Continue + } + throw new Error('Could not find More actions menu'); } /** * Click the captions button in the menu. - * Primary selector: div[id="closed-captions-button"] (confirmed by Recall.ai). + * Handles two UI variants: + * - Anonymous (light-meetings): direct div[id="closed-captions-button"] + * - Authenticated (full Teams): submenu "Captions & transcripts" → "Turn on live captions" */ private async _clickEnableCaptions(): Promise { - // Primary selector - confirmed by Recall.ai (Jan 2025) - const primarySelector = 'div[id="closed-captions-button"]'; + // Log visible menu items for debugging + await this._logVisibleMenuItems(); - try { - await this._page.waitForSelector(primarySelector, { timeout: 120000 }); - this._logger.info('Found "Captions" button'); - await this._page.click(primarySelector); - this._logger.info('Clicked "Captions" button'); - return; - } catch { - this._logger.info('Primary captions button selector not found, trying fallbacks...'); - } - - // Fallback selectors - const fallbackSelectors = [ - 'button:has-text("Turn on live captions")', - 'button:has-text("Live captions")', + // Strategy 1: Direct captions button (anonymous/light-meetings UI) + const directSelectors = [ + 'div[id="closed-captions-button"]', + '[data-tid="closed-captions-button"]', '[data-tid="captions-toggle"]', - 'button[aria-label*="captions" i]', - '[role="menuitem"]:has-text("captions")', - '[role="menuitemcheckbox"]:has-text("captions")', ]; - for (const selector of fallbackSelectors) { + for (const selector of directSelectors) { try { const button = await this._page.$(selector); if (button) { await button.click(); + this._logger.info(`Clicked direct captions button: ${selector}`); await this._page.waitForTimeout(1000); - this._logger.info(`Clicked enable captions (fallback: ${selector})`); return; } } catch { @@ -141,24 +128,163 @@ export class CaptionsProcedure { } } - // Try clicking away to close menu if captions not found + // Strategy 2: Authenticated Teams UI — "Captions & transcripts" submenu first + const submenuSelectors = [ + '[data-tid="captions-and-transcripts-button"]', + '[role="menuitem"]:has-text("Captions & transcripts")', + '[role="menuitem"]:has-text("Captions and transcripts")', + '[role="menuitem"]:has-text("Untertitel und Transkripte")', + '[role="menuitem"]:has-text("Untertitel")', + 'button:has-text("Captions & transcripts")', + 'button:has-text("Captions and transcripts")', + ]; + + for (const selector of submenuSelectors) { + try { + const item = await this._page.$(selector); + if (item) { + await item.click(); + this._logger.info(`Clicked captions submenu: ${selector}`); + await this._page.waitForTimeout(1500); + + // Now look for "Turn on live captions" inside the submenu/panel + const enableSelectors = [ + 'button:has-text("Turn on live captions")', + 'button:has-text("Live captions")', + 'button:has-text("Live-Untertitel aktivieren")', + 'button:has-text("Liveuntertitel")', + '[role="menuitem"]:has-text("Turn on live captions")', + '[role="menuitem"]:has-text("Live captions")', + '[role="menuitemcheckbox"]:has-text("captions")', + '[data-tid="toggle-captions"]', + ]; + + for (const enableSel of enableSelectors) { + try { + const enableBtn = await this._page.$(enableSel); + if (enableBtn) { + await enableBtn.click(); + this._logger.info(`Clicked enable captions: ${enableSel}`); + await this._page.waitForTimeout(1000); + return; + } + } catch { + // Continue + } + } + + this._logger.info('Opened captions submenu but could not find enable button'); + break; + } + } catch { + // Continue + } + } + + // Strategy 3: Generic text-based fallbacks + const textFallbacks = [ + 'button:has-text("Turn on live captions")', + 'button:has-text("Live captions")', + 'button[aria-label*="captions" i]', + '[role="menuitem"]:has-text("captions")', + '[role="menuitemcheckbox"]:has-text("captions")', + ]; + + for (const selector of textFallbacks) { + try { + const button = await this._page.$(selector); + if (button) { + await button.click(); + this._logger.info(`Clicked captions (text fallback): ${selector}`); + await this._page.waitForTimeout(1000); + return; + } + } catch { + // Continue + } + } + + // Strategy 4: DOM scan — find any element mentioning "caption" in the open menu + const found = await this._page.evaluate(() => { + const keywords = ['caption', 'captions', 'untertitel', 'live caption']; + const candidates = document.querySelectorAll( + '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], button, li, div[role="option"]' + ); + const results: string[] = []; + for (let i = 0; i < candidates.length; i++) { + const el = candidates[i] as HTMLElement; + const text = el.innerText?.toLowerCase()?.trim() || ''; + if (text && keywords.some(kw => text.includes(kw))) { + results.push(text.substring(0, 60)); + el.click(); + return { clicked: text.substring(0, 60), allMatches: results }; + } + } + return { clicked: null, allMatches: results }; + }); + + if (found.clicked) { + this._logger.info(`Clicked captions via DOM scan: "${found.clicked}"`); + await this._page.waitForTimeout(1500); + + // Check if this opened a submenu — look for "Turn on" or "enable" inside + const turnOnBtn = await this._page.$('button:has-text("Turn on"), [role="menuitem"]:has-text("Turn on")'); + if (turnOnBtn) { + await turnOnBtn.click(); + this._logger.info('Clicked "Turn on" in captions submenu'); + await this._page.waitForTimeout(1000); + } + return; + } + + // Nothing found await this._page.keyboard.press('Escape'); - this._logger.warn('Could not find captions option - may already be enabled or not available'); + this._logger.warn(`Could not find captions option. DOM scan matches: ${JSON.stringify(found.allMatches)}`); + } + + /** + * Log visible menu items for debugging when captions button is not found. + */ + private async _logVisibleMenuItems(): Promise { + try { + const menuItems = await this._page.evaluate(() => { + const items = document.querySelectorAll( + '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]' + ); + return Array.from(items).map(el => { + const text = (el as HTMLElement).innerText?.trim()?.substring(0, 50) || ''; + const tid = el.getAttribute('data-tid') || ''; + const id = el.id || ''; + return `[${tid || id || 'no-id'}] ${text}`; + }).filter(t => t.length > 5); + }); + this._logger.info(`Visible menu items (${menuItems.length}): ${JSON.stringify(menuItems)}`); + } catch { + // Not critical + } } /** * Wait for the captions container to become visible after enabling. - * Primary selector: div[data-tid="closed-caption-renderer-wrapper"] (confirmed by Recall.ai). */ private async _waitForCaptionsContainer(): Promise { - const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]'; + const containerSelectors = [ + 'div[data-tid="closed-caption-renderer-wrapper"]', + 'div[data-tid="live-captions-renderer"]', + '[data-tid="caption-area"]', + ]; - try { - await this._page.waitForSelector(containerSelector, { timeout: 120000 }); - this._logger.info('Found captions container'); - } catch { - this._logger.warn('Could not find captions container - captions may not have enabled'); + for (const selector of containerSelectors) { + try { + await this._page.waitForSelector(selector, { timeout: 15000 }); + this._logger.info(`Found captions container: ${selector}`); + return; + } catch { + // Try next + } } + + this._logger.warn('Could not find captions container - captions may not have enabled or may use a different selector'); } /** @@ -553,11 +679,24 @@ export class CaptionsProcedure { }); // Verify captions container is present - const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]'; - try { - await this._page.waitForSelector(containerSelector, { timeout: 120000 }); - } catch { - this._logger.warn('Captions container not found, subscribing anyway'); + const containerSelectors = [ + 'div[data-tid="closed-caption-renderer-wrapper"]', + 'div[data-tid="live-captions-renderer"]', + '[data-tid="caption-area"]', + ]; + let containerFound = false; + for (const sel of containerSelectors) { + try { + await this._page.waitForSelector(sel, { timeout: 10000 }); + containerFound = true; + this._logger.info(`Captions container found: ${sel}`); + break; + } catch { + // Try next + } + } + if (!containerFound) { + this._logger.warn('Captions container not found with known selectors, subscribing anyway'); } this._logger.info('Setting up MutationObserver for captions...');