PageSourceSearch

https://ppoagroup.com/js/conference.js?v=2026-05-29-r1

js ppoagroup.com collected 2026-09-24 14:50:20 UTC 4,819 bytes, 109 lines download raw bytes

1// PPOAGroup STAGE-1 AI conference harness — browser interop (additive, test-only).
2//
3// Deliberately SEPARATE from ppoaDialer (js/dialer.js): it owns its OWN Twilio.Device so the
4// manual dialer's device/handle is never touched. The operator joins the conference as an
5// OUTGOING Device.connect (same TwiML app → /outbound, which branches on the conferenceName
6// param), so NO change to the voice grant (IncomingAllow stays false) is required.
7//
8// Listen vs. take-over is local-mic mute:
9//   - join muted  → operator hears the AI↔destination conversation but is silent (live-listen)
10//   - un-mute     → operator speaks live on the conference (barge); pair with dropping the AI
11window.ppoaConference = (function () {
12    let device = null;
13    let activeCall = null;
14    let dotNetRef = null;
15
16    function reportEvent(name, payload) {
17        if (!dotNetRef) return;
18        try {
19            dotNetRef.invokeMethodAsync('OnConferenceEvent', name, payload || {});
20        } catch (e) {
21            // swallow — Blazor disposed before event reached
22        }
23    }
24
25    function isSdkUnavailable() {
26        if (window.__ppoaTwilioSdkLoadFailed === true) return true;
27        if (typeof window.Twilio === 'undefined') return true;
28        if (!window.Twilio.Device) return true;
29        return false;
30    }
31
32    function attachCallEvents(call) {
33        call.on('accept', () => reportEvent('accept', {}));
34        call.on('disconnect', () => { reportEvent('disconnect', {}); activeCall = null; });
35        call.on('cancel', () => reportEvent('cancel', {}));
36        call.on('error', (err) => reportEvent('error', { message: (err && err.message) || 'Unknown call error' }));
37    }
38
39    return {
40        isReady: function () { return !isSdkUnavailable(); },
41
42        register: function (token, dotNet) {
43            dotNetRef = dotNet;
44            if (isSdkUnavailable()) {
45                reportEvent('sdkUnavailable', { message: 'Voice library failed to load. Hard refresh (Ctrl+Shift+R).' });
46                return false;
47            }
48            try {
49                if (device) {
50                    device.updateToken(token);
51                    reportEvent('ready', {});
52                    return true;
53                }
54                device = new window.Twilio.Device(token, {
55                    logLevel: 'warn',
56                    closeProtection: true,
57                    edge: 'ashburn'
58                });
59                device.on('registered', () => reportEvent('ready', {}));
60                device.on('error', (err) => reportEvent('error', { message: (err && err.message) || 'Device error' }));
61                device.on('tokenWillExpire', () => reportEvent('tokenWillExpire', {}));
62                device.on('incoming', () => { /* harness is outbound-join only */ });
63                device.register();
64                return true;
65            } catch (e) {
66                reportEvent('error', { message: e.message || 'register failed' });
67                return false;
68            }
69        },
70
71        // Join the named conference, muted, for silent live-listen.
72        joinMuted: async function (conferenceName) {
73            if (isSdkUnavailable()) {
74                reportEvent('sdkUnavailable', { message: 'Voice library failed to load. Hard refresh (Ctrl+Shift+R).' });
75                return false;
76            }
77            if (!device) { reportEvent('error', { message: 'Device not registered' }); return false; }
78            try {
79                activeCall = await device.connect({ params: { conferenceName: conferenceName } });
80                attachCallEvents(activeCall);
81                // Join silent — operator listens; conference does not hear them until barge.
82                try { activeCall.mute(true); } catch { /* ignore */ }
83                reportEvent('joined', { muted: true });
84                return true;
85            } catch (e) {
86                reportEvent('error', { message: e.message || 'join failed' });
87                return false;
88            }
89        },
90
91        setMuted: function (shouldMute) {
92            if (activeCall) {
93                try { activeCall.mute(!!shouldMute); reportEvent('muteChanged', { muted: !!shouldMute }); } catch { /* ignore */ }
94            }
95        },
96
97        leave: function () {
98            if (activeCall) { try { activeCall.disconnect(); } catch { /* ignore */ } }
99            activeCall = null;
100        },
101
102        unregister: function () {
103            try { if (activeCall) activeCall.disconnect(); } catch { /* ignore */ }
104            try { if (device) device.unregister(); } catch { /* ignore */ }
105            activeCall = null;
106            device = null;
107        }
108    };
109})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.