callix-dialer-widget 2.5.1 → 2.7.0

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.
@@ -6,7 +6,7 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr
6
6
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
7
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
8
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
- import React, { useState, useMemo, useRef, useEffect, useCallback, forwardRef, useLayoutEffect, useId, createContext, useImperativeHandle, StrictMode, useContext } from "react";
9
+ import React, { createContext, useMemo, useRef, useEffect, useSyncExternalStore, useCallback, useContext, useState, forwardRef, useLayoutEffect, useId, useImperativeHandle, StrictMode } from "react";
10
10
  import require$$0$2, { createPortal } from "react-dom";
11
11
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
12
12
  function getAugmentedNamespace(n) {
@@ -43725,6 +43725,790 @@ function requireClientSdkReact() {
43725
43725
  return clientSdkReact;
43726
43726
  }
43727
43727
  var clientSdkReactExports = requireClientSdkReact();
43728
+ class EngineSignal {
43729
+ constructor(value) {
43730
+ this.value = value;
43731
+ this.listeners = /* @__PURE__ */ new Set();
43732
+ }
43733
+ get() {
43734
+ return this.value;
43735
+ }
43736
+ set(value) {
43737
+ if (JSON.stringify(value) === JSON.stringify(this.value)) return;
43738
+ this.value = value;
43739
+ for (const listener of this.listeners) listener(value);
43740
+ }
43741
+ subscribe(fn) {
43742
+ this.listeners.add(fn);
43743
+ return () => this.listeners.delete(fn);
43744
+ }
43745
+ }
43746
+ const INITIAL_TRANSCRIPT = { segments: [], status: null, insight: null, interims: 0 };
43747
+ const MAX_SEGMENTS = 300;
43748
+ const RECONNECT_MIN_MS = 500;
43749
+ const RECONNECT_MAX_MS = 5e3;
43750
+ function engineWsUrl(baseUrl) {
43751
+ const url = new URL(baseUrl, typeof window !== "undefined" ? window.location.href : "http://localhost");
43752
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
43753
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/ws`;
43754
+ url.search = "";
43755
+ url.hash = "";
43756
+ return url.toString();
43757
+ }
43758
+ class EngineClient {
43759
+ constructor(options) {
43760
+ this.options = options;
43761
+ this.userSignal = new EngineSignal(void 0);
43762
+ this.listeners = /* @__PURE__ */ new Set();
43763
+ this.sessionLostListeners = /* @__PURE__ */ new Set();
43764
+ this.viewWaiters = [];
43765
+ this.ws = null;
43766
+ this.reconnectDelay = RECONNECT_MIN_MS;
43767
+ this.reconnectTimer = null;
43768
+ this.closed = false;
43769
+ this.iceServers = [{ urls: "stun:stun.l.google.com:19302" }];
43770
+ this.pc = null;
43771
+ this.connectionId = null;
43772
+ this.remoteSet = false;
43773
+ this.pendingCandidates = [];
43774
+ this.micStream = null;
43775
+ this.audioSeq = 0;
43776
+ this.unlockListener = null;
43777
+ this.agentId = options.agentId;
43778
+ this.session = options.session ?? null;
43779
+ this.log = options.log ?? (() => void 0);
43780
+ this.audio = options.audioElement !== void 0 ? options.audioElement : createAudioElement();
43781
+ this.state = {
43782
+ wsState: "idle",
43783
+ tabId: null,
43784
+ view: null,
43785
+ engaged: false,
43786
+ pcState: "idle",
43787
+ needsUnlock: false,
43788
+ micError: null,
43789
+ lastError: null,
43790
+ inputMuted: false,
43791
+ outputMuted: false,
43792
+ outputVolume: 100,
43793
+ transcript: INITIAL_TRANSCRIPT,
43794
+ sessionLost: null
43795
+ };
43796
+ }
43797
+ // ------------------------------------------------------------------ store
43798
+ getState() {
43799
+ return this.state;
43800
+ }
43801
+ subscribe(listener) {
43802
+ this.listeners.add(listener);
43803
+ return () => {
43804
+ this.listeners.delete(listener);
43805
+ };
43806
+ }
43807
+ setState(patch) {
43808
+ this.state = { ...this.state, ...patch };
43809
+ for (const listener of this.listeners) listener();
43810
+ }
43811
+ get currentCall() {
43812
+ var _a2, _b;
43813
+ return ((_b = (_a2 = this.state.view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.call) ?? null;
43814
+ }
43815
+ // ------------------------------------------------------------------ conexão
43816
+ connect() {
43817
+ if (this.closed) this.closed = false;
43818
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return;
43819
+ const Impl = this.options.WebSocketImpl ?? WebSocket;
43820
+ this.setState({ wsState: "connecting" });
43821
+ const ws = new Impl(engineWsUrl(this.options.url));
43822
+ this.ws = ws;
43823
+ ws.onopen = () => {
43824
+ if (this.ws !== ws) return;
43825
+ this.reconnectDelay = RECONNECT_MIN_MS;
43826
+ this.setState({ wsState: "open" });
43827
+ const hello = { type: "hello", role: "tab", agentId: this.agentId, token: this.options.token, label: this.options.label ?? "Discador", ...this.session ? { session: this.session } : {} };
43828
+ this.send(hello);
43829
+ this.log("conectado ao host do motor");
43830
+ };
43831
+ ws.onmessage = (event) => {
43832
+ if (this.ws !== ws) return;
43833
+ let message;
43834
+ try {
43835
+ message = JSON.parse(String(event.data));
43836
+ } catch {
43837
+ return;
43838
+ }
43839
+ this.handle(message);
43840
+ };
43841
+ ws.onclose = () => {
43842
+ if (this.ws !== ws) return;
43843
+ this.ws = null;
43844
+ const wasEngaged = this.state.engaged;
43845
+ this.setState({ wsState: "closed", tabId: null, engaged: false });
43846
+ if (wasEngaged) this.stopAudio();
43847
+ if (this.closed) return;
43848
+ this.log(`conexão com o motor caiu; nova tentativa em ${this.reconnectDelay} ms`);
43849
+ this.reconnectTimer = setTimeout(() => {
43850
+ this.reconnectTimer = null;
43851
+ this.connect();
43852
+ }, this.reconnectDelay);
43853
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
43854
+ };
43855
+ ws.onerror = () => void 0;
43856
+ }
43857
+ /** fecha de vez (sem reconectar) e solta o áudio */
43858
+ close() {
43859
+ this.closed = true;
43860
+ if (this.reconnectTimer) {
43861
+ clearTimeout(this.reconnectTimer);
43862
+ this.reconnectTimer = null;
43863
+ }
43864
+ this.stopAudio();
43865
+ const ws = this.ws;
43866
+ this.ws = null;
43867
+ if (ws) {
43868
+ ws.onclose = null;
43869
+ ws.close();
43870
+ }
43871
+ for (const waiter of this.viewWaiters.splice(0)) clearTimeout(waiter.timer);
43872
+ this.setState({ wsState: "closed", tabId: null, engaged: false });
43873
+ }
43874
+ /** a view atual, ou a próxima que o host mandar (welcome/view) */
43875
+ waitForView(timeoutMs = 15e3) {
43876
+ if (this.state.view && this.state.tabId) return Promise.resolve(this.state.view);
43877
+ return new Promise((resolve, reject) => {
43878
+ const timer = setTimeout(() => {
43879
+ const index = this.viewWaiters.findIndex((w) => w.timer === timer);
43880
+ if (index >= 0) this.viewWaiters.splice(index, 1);
43881
+ reject(new Error("o motor não respondeu"));
43882
+ }, timeoutMs);
43883
+ this.viewWaiters.push({ resolve, timer });
43884
+ });
43885
+ }
43886
+ send(message) {
43887
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
43888
+ }
43889
+ handle(message) {
43890
+ switch (message.type) {
43891
+ case "welcome": {
43892
+ if (Array.isArray(message.iceServers) && message.iceServers.length > 0) this.iceServers = message.iceServers;
43893
+ this.setState({ tabId: message.tabId });
43894
+ this.applyView(message.view);
43895
+ this.log(`sou a aba ${message.tabId}`);
43896
+ if (this.options.autoEngage !== false && message.view.engagedTabId !== message.tabId) this.engage();
43897
+ break;
43898
+ }
43899
+ case "view":
43900
+ this.applyView(message.view);
43901
+ break;
43902
+ case "engage":
43903
+ this.setState({ engaged: message.engaged });
43904
+ if (message.engaged) void this.startAudio();
43905
+ else this.stopAudio();
43906
+ break;
43907
+ case "signal":
43908
+ void this.onSignal(message.payload);
43909
+ break;
43910
+ case "error":
43911
+ this.setState({ lastError: message.message });
43912
+ this.log(`erro do host: ${message.message}`);
43913
+ break;
43914
+ case "transcript":
43915
+ this.pushSegment(message.segment);
43916
+ break;
43917
+ case "transcript-sync":
43918
+ this.setState({ transcript: { segments: message.segments ?? [], status: message.status ?? null, insight: message.insight ?? null, interims: 0 } });
43919
+ break;
43920
+ case "insight": {
43921
+ const current = this.state.transcript.insight;
43922
+ if (!current || message.insight.revision >= current.revision || message.insight.callId !== current.callId) {
43923
+ this.setState({ transcript: { ...this.state.transcript, insight: message.insight } });
43924
+ }
43925
+ break;
43926
+ }
43927
+ case "transcript-status":
43928
+ this.onTranscriptStatus(message.status);
43929
+ break;
43930
+ }
43931
+ }
43932
+ applyView(view) {
43933
+ var _a2, _b;
43934
+ const previous = this.state.view;
43935
+ const lost = view.sessionLost ?? null;
43936
+ const newlyLost = Boolean(lost) && !(previous == null ? void 0 : previous.sessionLost);
43937
+ if (newlyLost) {
43938
+ this.session = null;
43939
+ this.log(`o Callix derrubou a sessão: ${(lost == null ? void 0 : lost.message) ?? ""}`);
43940
+ }
43941
+ const user = (_a2 = view.snapshot) == null ? void 0 : _a2.leg.user;
43942
+ this.userSignal.set(user ? { ...user, sessionId: 0 } : void 0);
43943
+ const call = (_b = view.snapshot) == null ? void 0 : _b.leg.call;
43944
+ const patch = { view, sessionLost: lost ? { originator: lost.originator, message: lost.message } : null };
43945
+ if (call && typeof call.muted === "boolean" && call.muted !== this.state.inputMuted && call.state !== "ended") patch.inputMuted = call.muted;
43946
+ this.setState(patch);
43947
+ for (const waiter of this.viewWaiters.splice(0)) {
43948
+ clearTimeout(waiter.timer);
43949
+ waiter.resolve(view);
43950
+ }
43951
+ if (newlyLost && lost) for (const listener of this.sessionLostListeners) listener(lost);
43952
+ }
43953
+ onSessionLost(listener) {
43954
+ this.sessionLostListeners.add(listener);
43955
+ return () => {
43956
+ this.sessionLostListeners.delete(listener);
43957
+ };
43958
+ }
43959
+ // ------------------------------------------------------------------ sessão e comandos
43960
+ /** modo sdk: o widget fez login no back-end e entrega a sessão ao host */
43961
+ setSession(session) {
43962
+ this.session = session;
43963
+ this.send({ type: "session", session });
43964
+ }
43965
+ engage() {
43966
+ this.send({ type: "engage" });
43967
+ }
43968
+ release() {
43969
+ this.send({ type: "release" });
43970
+ }
43971
+ command(command) {
43972
+ this.send({ type: "command", command });
43973
+ this.log(`comando: ${command.name}${command.phone ? ` ${command.phone}` : ""}`);
43974
+ }
43975
+ // ------------------------------------------------------------------ áudio
43976
+ setInputMuted(muted) {
43977
+ if (this.micStream) for (const track of this.micStream.getAudioTracks()) track.enabled = !muted;
43978
+ this.setState({ inputMuted: muted });
43979
+ if (this.currentCall) this.command({ name: muted ? "mute" : "unmute" });
43980
+ }
43981
+ setOutputMuted(muted) {
43982
+ if (this.audio) this.audio.muted = muted;
43983
+ this.setState({ outputMuted: muted });
43984
+ }
43985
+ setOutputVolume(volume) {
43986
+ const clamped = Math.max(0, Math.min(100, Math.round(volume)));
43987
+ if (this.audio) this.audio.volume = clamped / 100;
43988
+ this.setState({ outputVolume: clamped });
43989
+ }
43990
+ async setOutputDevice(deviceId) {
43991
+ const audio = this.audio;
43992
+ if (!(audio == null ? void 0 : audio.setSinkId)) throw new Error("este navegador não permite escolher a saída de som");
43993
+ await audio.setSinkId(deviceId);
43994
+ }
43995
+ /** depois de um toque do agente: tenta tocar o som do motor que o navegador segurou */
43996
+ async unlockAudio() {
43997
+ if (!this.audio || !this.audio.srcObject) return;
43998
+ try {
43999
+ await this.audio.play();
44000
+ this.setState({ needsUnlock: false });
44001
+ } catch {
44002
+ this.setState({ needsUnlock: true });
44003
+ }
44004
+ }
44005
+ async startAudio() {
44006
+ const seq = ++this.audioSeq;
44007
+ this.stopAudio();
44008
+ const mediaDevices = this.options.mediaDevices === void 0 ? typeof navigator !== "undefined" ? navigator.mediaDevices : null : this.options.mediaDevices;
44009
+ if (!(mediaDevices == null ? void 0 : mediaDevices.getUserMedia)) {
44010
+ this.setState({ micError: "Este navegador não permite usar o microfone pela página." });
44011
+ return;
44012
+ }
44013
+ let stream;
44014
+ try {
44015
+ stream = await mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
44016
+ } catch (error2) {
44017
+ const message = `Microfone indisponível: ${error2.message}`;
44018
+ this.setState({ micError: message });
44019
+ this.log(message);
44020
+ return;
44021
+ }
44022
+ if (seq !== this.audioSeq || !this.state.engaged) {
44023
+ for (const track of stream.getTracks()) track.stop();
44024
+ return;
44025
+ }
44026
+ this.micStream = stream;
44027
+ for (const track of stream.getAudioTracks()) track.enabled = !this.state.inputMuted;
44028
+ this.setState({ micError: null });
44029
+ const Impl = this.options.PeerConnectionImpl ?? RTCPeerConnection;
44030
+ const connection = new Impl({ iceServers: this.iceServers });
44031
+ this.pc = connection;
44032
+ const connectionId = Math.random().toString(36).slice(2, 10);
44033
+ this.connectionId = connectionId;
44034
+ this.remoteSet = false;
44035
+ this.pendingCandidates = [];
44036
+ for (const track of stream.getAudioTracks()) connection.addTrack(track, stream);
44037
+ connection.onicecandidate = (event) => {
44038
+ this.send({ type: "signal", payload: { connectionId, candidate: event.candidate ? event.candidate.toJSON() : null } });
44039
+ };
44040
+ connection.ontrack = (event) => {
44041
+ const remote = event.streams[0] ?? new MediaStream([event.track]);
44042
+ if (this.audio) {
44043
+ this.audio.srcObject = remote;
44044
+ this.audio.muted = this.state.outputMuted;
44045
+ this.audio.volume = this.state.outputVolume / 100;
44046
+ this.audio.play().then(() => this.setState({ needsUnlock: false })).catch(() => {
44047
+ this.setState({ needsUnlock: true });
44048
+ this.armUnlock();
44049
+ });
44050
+ }
44051
+ this.log("áudio do motor chegou");
44052
+ };
44053
+ connection.onconnectionstatechange = () => {
44054
+ if (this.pc !== connection) return;
44055
+ this.setState({ pcState: connection.connectionState });
44056
+ this.log(`WebRTC: ${connection.connectionState}`);
44057
+ if (connection.connectionState === "failed" && this.state.engaged) {
44058
+ this.log("conexão de áudio com o motor falhou; renegociando");
44059
+ void this.startAudio();
44060
+ }
44061
+ };
44062
+ const offer = await connection.createOffer();
44063
+ if (this.pc !== connection) return;
44064
+ await connection.setLocalDescription(offer);
44065
+ this.send({ type: "signal", payload: { connectionId, description: { type: offer.type, sdp: offer.sdp } } });
44066
+ this.setState({ pcState: connection.connectionState });
44067
+ }
44068
+ stopAudio() {
44069
+ if (this.pc) {
44070
+ this.pc.onconnectionstatechange = null;
44071
+ this.pc.onicecandidate = null;
44072
+ this.pc.ontrack = null;
44073
+ this.pc.close();
44074
+ this.pc = null;
44075
+ }
44076
+ this.connectionId = null;
44077
+ if (this.micStream) {
44078
+ for (const track of this.micStream.getTracks()) track.stop();
44079
+ this.micStream = null;
44080
+ }
44081
+ if (this.audio) this.audio.srcObject = null;
44082
+ if (this.state.pcState !== "idle") this.setState({ pcState: "idle" });
44083
+ }
44084
+ async onSignal(payload) {
44085
+ const connection = this.pc;
44086
+ if (!connection) return;
44087
+ if (payload.connectionId && payload.connectionId !== this.connectionId) return;
44088
+ try {
44089
+ if (payload.description) {
44090
+ await connection.setRemoteDescription(payload.description);
44091
+ this.remoteSet = true;
44092
+ for (const candidate of this.pendingCandidates.splice(0)) await connection.addIceCandidate(candidate).catch(() => void 0);
44093
+ }
44094
+ if (payload.candidate !== void 0) {
44095
+ const candidate = payload.candidate ?? void 0;
44096
+ if (!this.remoteSet && candidate) this.pendingCandidates.push(candidate);
44097
+ else await connection.addIceCandidate(candidate).catch(() => void 0);
44098
+ }
44099
+ } catch (error2) {
44100
+ this.log(`sinalização: ${error2.message}`);
44101
+ }
44102
+ }
44103
+ /** o próximo toque/tecla na página libera o som (política de autoplay) */
44104
+ armUnlock() {
44105
+ if (this.unlockListener || typeof document === "undefined") return;
44106
+ const listener = () => {
44107
+ document.removeEventListener("pointerdown", listener, true);
44108
+ document.removeEventListener("keydown", listener, true);
44109
+ this.unlockListener = null;
44110
+ void this.unlockAudio();
44111
+ };
44112
+ this.unlockListener = listener;
44113
+ document.addEventListener("pointerdown", listener, true);
44114
+ document.addEventListener("keydown", listener, true);
44115
+ }
44116
+ // ------------------------------------------------------------------ transcrição
44117
+ pushSegment(segment) {
44118
+ const current = this.state.transcript;
44119
+ let segments = current.segments;
44120
+ let insight = current.insight;
44121
+ let interims = current.interims;
44122
+ if (segments.length && segments[0].callId !== segment.callId) {
44123
+ segments = [];
44124
+ insight = null;
44125
+ interims = 0;
44126
+ }
44127
+ const index = segments.findIndex((s) => s.id === segment.id);
44128
+ const next = index >= 0 ? segments.map((s, i2) => i2 === index ? segment : s) : [...segments, segment];
44129
+ if (next.length > MAX_SEGMENTS) next.shift();
44130
+ if (segment.interim) interims += 1;
44131
+ this.setState({ transcript: { ...current, segments: next, insight, interims } });
44132
+ }
44133
+ onTranscriptStatus(status) {
44134
+ const current = this.state.transcript;
44135
+ if (!current.status || current.status.recordingId === status.recordingId || status.live === "running") {
44136
+ const fresh = current.status && current.status.recordingId !== status.recordingId;
44137
+ this.setState({ transcript: { ...current, status, ...fresh ? { segments: [], insight: null, interims: 0 } : {} } });
44138
+ }
44139
+ }
44140
+ }
44141
+ function createAudioElement() {
44142
+ if (typeof document === "undefined") return null;
44143
+ const audio = document.createElement("audio");
44144
+ audio.autoplay = true;
44145
+ audio.setAttribute("playsinline", "true");
44146
+ audio.style.display = "none";
44147
+ audio.dataset.callixEngineAudio = "true";
44148
+ document.body.appendChild(audio);
44149
+ return audio;
44150
+ }
44151
+ const EngineClientContext = createContext(null);
44152
+ function EngineClientProvider({ client: client2, children: children2 }) {
44153
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(EngineClientContext.Provider, { value: client2, children: children2 });
44154
+ }
44155
+ function useEngineClient() {
44156
+ return useContext(EngineClientContext);
44157
+ }
44158
+ function useEngineState(client2) {
44159
+ return useSyncExternalStore(client2.subscribe.bind(client2), client2.getState.bind(client2), client2.getState.bind(client2));
44160
+ }
44161
+ const ENGINE_BREAKS = [{ id: 0, name: "Pausa" }];
44162
+ function toCallQualification(q) {
44163
+ return { id: q.id, name: q.name, isSystemQualification: false, systemQualificationType: 0 };
44164
+ }
44165
+ function qualificationsFromView(view, kind) {
44166
+ var _a2;
44167
+ return (((_a2 = view == null ? void 0 : view.snapshot) == null ? void 0 : _a2.leg.qualifications) ?? []).filter((q) => q.kind === kind).map(toCallQualification);
44168
+ }
44169
+ class EngineCall {
44170
+ constructor(client2, snapshot) {
44171
+ this.client = client2;
44172
+ this.listeners = /* @__PURE__ */ new Map();
44173
+ this.snapshot = snapshot;
44174
+ }
44175
+ get id() {
44176
+ return this.snapshot.id;
44177
+ }
44178
+ get direction() {
44179
+ return this.snapshot.direction;
44180
+ }
44181
+ get data() {
44182
+ var _a2;
44183
+ return {
44184
+ destinationNumber: (_a2 = this.snapshot.contact) == null ? void 0 : _a2.phone,
44185
+ campaignContactPhoneId: this.snapshot.campaignContactPhoneId,
44186
+ answeredAt: this.snapshot.answeredAt ? new Date(this.snapshot.answeredAt) : void 0
44187
+ };
44188
+ }
44189
+ get callerNumber() {
44190
+ var _a2;
44191
+ return (_a2 = this.snapshot.contact) == null ? void 0 : _a2.phone;
44192
+ }
44193
+ get peerDisplayName() {
44194
+ var _a2;
44195
+ return (_a2 = this.snapshot.contact) == null ? void 0 : _a2.name;
44196
+ }
44197
+ get isEstablished() {
44198
+ return this.snapshot.state === "active";
44199
+ }
44200
+ get isEnded() {
44201
+ return this.snapshot.state === "ended";
44202
+ }
44203
+ /** segundos desde o atendimento (ou desde o início, enquanto toca) */
44204
+ get currentDurationSeconds() {
44205
+ const from = this.snapshot.answeredAt ?? this.snapshot.startedAt;
44206
+ return Math.max(0, Math.floor((Date.now() - from) / 1e3));
44207
+ }
44208
+ answer() {
44209
+ this.client.command({ name: "answer" });
44210
+ }
44211
+ reject() {
44212
+ this.client.command({ name: "reject" });
44213
+ }
44214
+ hangup() {
44215
+ this.client.command({ name: "hangup" });
44216
+ }
44217
+ isOnHold() {
44218
+ return { local: false, remote: false };
44219
+ }
44220
+ async hold() {
44221
+ }
44222
+ async unhold() {
44223
+ }
44224
+ isMuted() {
44225
+ return this.client.getState().inputMuted;
44226
+ }
44227
+ mute() {
44228
+ this.client.setInputMuted(true);
44229
+ }
44230
+ unmute() {
44231
+ this.client.setInputMuted(false);
44232
+ }
44233
+ sendDtmf(tones) {
44234
+ this.client.command({ name: "dtmf", tones });
44235
+ }
44236
+ on(event, listener) {
44237
+ let set = this.listeners.get(event);
44238
+ if (!set) {
44239
+ set = /* @__PURE__ */ new Set();
44240
+ this.listeners.set(event, set);
44241
+ }
44242
+ set.add(listener);
44243
+ return this;
44244
+ }
44245
+ once(event, listener) {
44246
+ const wrapped = (...args) => {
44247
+ this.off(event, wrapped);
44248
+ listener(...args);
44249
+ };
44250
+ return this.on(event, wrapped);
44251
+ }
44252
+ off(event, listener) {
44253
+ var _a2;
44254
+ (_a2 = this.listeners.get(event)) == null ? void 0 : _a2.delete(listener);
44255
+ return this;
44256
+ }
44257
+ toCallData() {
44258
+ var _a2;
44259
+ return {
44260
+ id: this.snapshot.id,
44261
+ direction: this.snapshot.direction,
44262
+ destinationNumber: ((_a2 = this.snapshot.contact) == null ? void 0 : _a2.phone) ?? "",
44263
+ answeredAt: this.snapshot.answeredAt ? new Date(this.snapshot.answeredAt).toISOString() : void 0,
44264
+ campaignContactPhoneId: this.snapshot.campaignContactPhoneId,
44265
+ isMuted: this.snapshot.muted
44266
+ };
44267
+ }
44268
+ }
44269
+ function operatorStateFromView(view, call) {
44270
+ var _a2, _b, _c;
44271
+ if (!view) return void 0;
44272
+ if (((_a2 = view.sessionLost) == null ? void 0 : _a2.originator) === "anotherDevice") return { state: "startupError", error: "userIsAlreadyConnectedOnAnotherDevice" };
44273
+ const leg = (_b = view.snapshot) == null ? void 0 : _b.leg;
44274
+ if (!leg) {
44275
+ if (view.engine === "starting") return { state: "starting" };
44276
+ if (view.engine === "running") return { state: "starting" };
44277
+ return { state: "offline" };
44278
+ }
44279
+ const asCall = call ?? void 0;
44280
+ switch (leg.status) {
44281
+ case "starting":
44282
+ return { state: "starting" };
44283
+ case "available":
44284
+ return { state: "idle" };
44285
+ case "ringing":
44286
+ return { state: "callRinging", call: asCall };
44287
+ case "dialing":
44288
+ return ((_c = leg.call) == null ? void 0 : _c.state) === "ringing" ? { state: "manualCallRinging", call: asCall } : { state: "manualCallSetup", call: asCall };
44289
+ case "inCall":
44290
+ return { state: "callInProgress", call: asCall };
44291
+ case "afterCall":
44292
+ return { state: "afterCall" };
44293
+ case "onBreak":
44294
+ return { state: "onBreak", break: ENGINE_BREAKS[0] };
44295
+ case "offline":
44296
+ return { state: "offline" };
44297
+ case "error":
44298
+ return { state: "startupError", error: "unknownError" };
44299
+ }
44300
+ }
44301
+ function callInfoFromView(view) {
44302
+ var _a2;
44303
+ const leg = (_a2 = view == null ? void 0 : view.snapshot) == null ? void 0 : _a2.leg;
44304
+ const call = leg == null ? void 0 : leg.call;
44305
+ if (!leg || !call) return void 0;
44306
+ const successQualifications = qualificationsFromView(view, "success");
44307
+ const discardQualifications = qualificationsFromView(view, "discard");
44308
+ const contact = call.contact;
44309
+ if (contact == null ? void 0 : contact.campaign) {
44310
+ const info = {
44311
+ campaign: {
44312
+ id: 0,
44313
+ name: contact.campaign,
44314
+ type: 0,
44315
+ requireCallComments: false,
44316
+ enableTransfer: false,
44317
+ enableScheduledCall: false,
44318
+ successQualifications,
44319
+ discardQualifications,
44320
+ previewDuration: "",
44321
+ showCampaignModelOnService: false,
44322
+ skipCallQualification: false
44323
+ },
44324
+ campaignList: { id: 0, name: "" },
44325
+ campaignContact: { id: call.campaignContactPhoneId ?? 0, label: contact.name, phoneNumber: contact.phone }
44326
+ };
44327
+ return { type: "campaign", info };
44328
+ }
44329
+ return { type: "manual", info: { successQualifications, discardQualifications } };
44330
+ }
44331
+ function useEngineCall(client2) {
44332
+ var _a2, _b;
44333
+ const state = useEngineState(client2);
44334
+ const snapshot = ((_b = (_a2 = state.view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.call) ?? null;
44335
+ const ref = useRef(null);
44336
+ if (!snapshot || snapshot.state === "ended") {
44337
+ ref.current = null;
44338
+ } else if (!ref.current || ref.current.id !== snapshot.id) {
44339
+ ref.current = new EngineCall(client2, snapshot);
44340
+ } else {
44341
+ ref.current.snapshot = snapshot;
44342
+ }
44343
+ return ref.current;
44344
+ }
44345
+ function useEngineOperatorState(client2) {
44346
+ const state = useEngineState(client2);
44347
+ const call = useEngineCall(client2);
44348
+ return useMemo(() => operatorStateFromView(state.view, call), [state.view, call]);
44349
+ }
44350
+ function useEngineCurrentCall(client2) {
44351
+ const call = useEngineCall(client2);
44352
+ return call ?? void 0;
44353
+ }
44354
+ function useEngineCurrentCallInfo(client2) {
44355
+ var _a2, _b, _c, _d, _e;
44356
+ const state = useEngineState(client2);
44357
+ const view = state.view;
44358
+ const key = JSON.stringify([(_b = (_a2 = view == null ? void 0 : view.snapshot) == null ? void 0 : _a2.leg.call) == null ? void 0 : _b.id, (_d = (_c = view == null ? void 0 : view.snapshot) == null ? void 0 : _c.leg.call) == null ? void 0 : _d.contact, (_e = view == null ? void 0 : view.snapshot) == null ? void 0 : _e.leg.qualifications]);
44359
+ return useMemo(() => callInfoFromView(view), [key]);
44360
+ }
44361
+ function useEngineQualifications(client2, kind) {
44362
+ var _a2, _b;
44363
+ const state = useEngineState(client2);
44364
+ const raw = ((_b = (_a2 = state.view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.qualifications) ?? [];
44365
+ const key = JSON.stringify(raw);
44366
+ return useMemo(() => qualificationsFromView(state.view, kind), [key, kind]);
44367
+ }
44368
+ function useEngineControls(client2) {
44369
+ return useMemo(
44370
+ () => ({
44371
+ becomeAvailable: () => {
44372
+ var _a2, _b;
44373
+ const status = (_b = (_a2 = client2.getState().view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.status;
44374
+ if (status === "onBreak") client2.command({ name: "resume" });
44375
+ },
44376
+ goOffline: () => void 0,
44377
+ enterOnBreak: () => client2.command({ name: "pause" }),
44378
+ makeManualCall: (phoneNumber) => client2.command({ name: "dial", phone: phoneNumber }),
44379
+ finishAfterCall: (data) => {
44380
+ if (typeof data.qualificationId === "number") client2.command({ name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44381
+ else client2.command({ name: "finish" });
44382
+ }
44383
+ }),
44384
+ [client2]
44385
+ );
44386
+ }
44387
+ function useEngineCurrentCallControls(client2) {
44388
+ return useMemo(
44389
+ () => ({
44390
+ answer: () => client2.command({ name: "answer" }),
44391
+ reject: () => client2.command({ name: "reject" }),
44392
+ hangup: () => client2.command({ name: "hangup" }),
44393
+ isOnHold: () => ({ local: false, remote: false }),
44394
+ hold: async () => void 0,
44395
+ unhold: async () => void 0,
44396
+ isMuted: () => client2.getState().inputMuted,
44397
+ mute: () => client2.setInputMuted(true),
44398
+ unmute: () => client2.setInputMuted(false)
44399
+ }),
44400
+ [client2]
44401
+ );
44402
+ }
44403
+ function useEngineAudioInputMuted(client2) {
44404
+ const state = useEngineState(client2);
44405
+ const set = useCallback((muted) => client2.setInputMuted(muted), [client2]);
44406
+ return [state.inputMuted, set];
44407
+ }
44408
+ function useEngineAudioOutputMuted(client2) {
44409
+ const state = useEngineState(client2);
44410
+ const set = useCallback((muted) => client2.setOutputMuted(muted), [client2]);
44411
+ return [state.outputMuted, set];
44412
+ }
44413
+ function useEngineAudioOutputVolume(client2) {
44414
+ const state = useEngineState(client2);
44415
+ const set = useCallback((volume) => client2.setOutputVolume(volume), [client2]);
44416
+ return [state.outputVolume, set];
44417
+ }
44418
+ function useEngineEventListener(client2, event, listener) {
44419
+ const ref = useRef(listener);
44420
+ ref.current = listener;
44421
+ useEffect(() => {
44422
+ return client2.onSessionLost((lost) => {
44423
+ ref.current({ originator: lost.originator });
44424
+ });
44425
+ }, [client2, event]);
44426
+ }
44427
+ const operators = /* @__PURE__ */ new WeakMap();
44428
+ function engineOperator(client2) {
44429
+ let operator = operators.get(client2);
44430
+ if (!operator) {
44431
+ const breaks = new EngineSignal(ENGINE_BREAKS);
44432
+ operator = {
44433
+ breaks,
44434
+ user: client2.userSignal,
44435
+ audioOutput: {
44436
+ setAudioOutputDevice: (id) => void client2.setOutputDevice(id).catch(() => void 0),
44437
+ setVolume: (volume) => client2.setOutputVolume(volume),
44438
+ setMute: (muted) => client2.setOutputMuted(muted)
44439
+ },
44440
+ stop: () => void 0,
44441
+ goOffline: () => void 0,
44442
+ becomeAvailable: () => {
44443
+ var _a2, _b;
44444
+ if (((_b = (_a2 = client2.getState().view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.status) === "onBreak") client2.command({ name: "resume" });
44445
+ },
44446
+ enterOnBreak: () => client2.command({ name: "pause" }),
44447
+ makeManualCall: (phoneNumber) => client2.command({ name: "dial", phone: phoneNumber }),
44448
+ finishAfterCall: (data) => {
44449
+ if (typeof data.qualificationId === "number") client2.command({ name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44450
+ else client2.command({ name: "finish" });
44451
+ }
44452
+ };
44453
+ operators.set(client2, operator);
44454
+ }
44455
+ return operator;
44456
+ }
44457
+ function useEngineOperator(client2) {
44458
+ return useMemo(() => engineOperator(client2), [client2]);
44459
+ }
44460
+ function useEngineTranscript(client2) {
44461
+ return useEngineState(client2).transcript;
44462
+ }
44463
+ function useCallOperator() {
44464
+ const engine = useContext(EngineClientContext);
44465
+ return engine ? useEngineOperator(engine) : clientSdkReactExports.useCallOperator();
44466
+ }
44467
+ function useCallOperatorState() {
44468
+ const engine = useContext(EngineClientContext);
44469
+ return engine ? useEngineOperatorState(engine) : clientSdkReactExports.useCallOperatorState();
44470
+ }
44471
+ function useCallOperatorControls() {
44472
+ const engine = useContext(EngineClientContext);
44473
+ return engine ? useEngineControls(engine) : clientSdkReactExports.useCallOperatorControls();
44474
+ }
44475
+ function useCallOperatorCurrentCall() {
44476
+ const engine = useContext(EngineClientContext);
44477
+ return engine ? useEngineCurrentCall(engine) : clientSdkReactExports.useCallOperatorCurrentCall();
44478
+ }
44479
+ function useCallOperatorCurrentCallInfo() {
44480
+ const engine = useContext(EngineClientContext);
44481
+ return engine ? useEngineCurrentCallInfo(engine) : clientSdkReactExports.useCallOperatorCurrentCallInfo();
44482
+ }
44483
+ function useCallOperatorSuccessQualifications() {
44484
+ const engine = useContext(EngineClientContext);
44485
+ return engine ? useEngineQualifications(engine, "success") : clientSdkReactExports.useCallOperatorSuccessQualifications();
44486
+ }
44487
+ function useCallOperatorDiscardQualifications() {
44488
+ const engine = useContext(EngineClientContext);
44489
+ return engine ? useEngineQualifications(engine, "discard") : clientSdkReactExports.useCallOperatorDiscardQualifications();
44490
+ }
44491
+ function useCallOperatorCurrentCallControls() {
44492
+ const engine = useContext(EngineClientContext);
44493
+ return engine ? useEngineCurrentCallControls(engine) : clientSdkReactExports.useCallOperatorCurrentCallControls();
44494
+ }
44495
+ function useCallOperatorAudioInputMuted() {
44496
+ const engine = useContext(EngineClientContext);
44497
+ return engine ? useEngineAudioInputMuted(engine) : clientSdkReactExports.useCallOperatorAudioInputMuted();
44498
+ }
44499
+ function useCallOperatorAudioOutputMuted() {
44500
+ const engine = useContext(EngineClientContext);
44501
+ return engine ? useEngineAudioOutputMuted(engine) : clientSdkReactExports.useCallOperatorAudioOutputMuted();
44502
+ }
44503
+ function useCallOperatorAudioOutputVolume() {
44504
+ const engine = useContext(EngineClientContext);
44505
+ return engine ? useEngineAudioOutputVolume(engine) : clientSdkReactExports.useCallOperatorAudioOutputVolume();
44506
+ }
44507
+ function useCallOperatorEventListener(event, listener) {
44508
+ const engine = useContext(EngineClientContext);
44509
+ if (engine) useEngineEventListener(engine, event, listener);
44510
+ else clientSdkReactExports.useCallOperatorEventListener(event, listener);
44511
+ }
43728
44512
  var DefaultContext = {
43729
44513
  color: void 0,
43730
44514
  size: void 0,
@@ -48745,8 +49529,8 @@ function formatPhoneValue(raw) {
48745
49529
  }
48746
49530
  function CallInfoDisplay() {
48747
49531
  var _a2;
48748
- const callInfo = clientSdkReactExports.useCallOperatorCurrentCallInfo();
48749
- const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
49532
+ const callInfo = useCallOperatorCurrentCallInfo();
49533
+ const currentCall = useCallOperatorCurrentCall();
48750
49534
  const campaignContactPhoneId = ((_a2 = currentCall == null ? void 0 : currentCall.data) == null ? void 0 : _a2.campaignContactPhoneId) ?? null;
48751
49535
  if ((callInfo == null ? void 0 : callInfo.type) === "campaign") {
48752
49536
  return /* @__PURE__ */ jsxRuntimeExports.jsx(CampaignCallInfoDisplay, { campaignCallInfo: callInfo.info, campaignContactPhoneId });
@@ -48892,7 +49676,7 @@ function useSignal(signal) {
48892
49676
  return value;
48893
49677
  }
48894
49678
  function useOperatorBreak() {
48895
- const { enterOnBreak, becomeAvailable } = clientSdkReactExports.useCallOperatorControls();
49679
+ const { enterOnBreak, becomeAvailable } = useCallOperatorControls();
48896
49680
  return {
48897
49681
  // Use the SDK's native method
48898
49682
  goOnBreak: enterOnBreak,
@@ -51592,6 +52376,8 @@ function FloatingDialer({
51592
52376
  sessionStatus = null,
51593
52377
  loginConfirmation = null,
51594
52378
  onCancelLoginConfirmation,
52379
+ onTakeOverSession,
52380
+ onLogoutAllTabs,
51595
52381
  variant = "default",
51596
52382
  portalTarget = null
51597
52383
  }) {
@@ -51944,7 +52730,9 @@ function FloatingDialer({
51944
52730
  error: sessionError,
51945
52731
  statusMessage: sessionStatus,
51946
52732
  confirmation: loginConfirmation,
51947
- onCancelConfirmation: onCancelLoginConfirmation
52733
+ onCancelConfirmation: onCancelLoginConfirmation,
52734
+ onTakeOver: onTakeOverSession,
52735
+ onLogoutAll: onLogoutAllTabs
51948
52736
  }
51949
52737
  );
51950
52738
  if (isMiniMode) {
@@ -52080,7 +52868,7 @@ function FloatingDialer({
52080
52868
  }
52081
52869
  );
52082
52870
  }
52083
- function LoginForm({ defaultValue, onSubmit, isLoading, error: error2, statusMessage, confirmation, onCancelConfirmation }) {
52871
+ function LoginForm({ defaultValue, onSubmit, isLoading, error: error2, statusMessage, confirmation, onCancelConfirmation, onTakeOver, onLogoutAll }) {
52084
52872
  const [value, setValue] = useState(defaultValue);
52085
52873
  const [touched, setTouched] = useState(false);
52086
52874
  useEffect(() => {
@@ -52111,8 +52899,41 @@ function LoginForm({ defaultValue, onSubmit, isLoading, error: error2, statusMes
52111
52899
  }
52112
52900
  );
52113
52901
  }
52902
+ if (confirmation == null ? void 0 : confirmation.elsewhere) {
52903
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: tw`space-y-4`, "data-login-state": "elsewhere", children: [
52904
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: tw`space-y-2 text-center`, children: [
52905
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-[11px] font-semibold uppercase tracking-wide text-slate-500`, children: "Discador ativo em outra aba" }),
52906
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: tw`inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-sm font-semibold text-slate-800`, children: [
52907
+ /* @__PURE__ */ jsxRuntimeExports.jsx(FiUser, { className: tw`text-primary` }),
52908
+ confirmation.username
52909
+ ] })
52910
+ ] }),
52911
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-xs text-red-500`, children: error2 }),
52912
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
52913
+ "button",
52914
+ {
52915
+ type: "button",
52916
+ id: "takeOverBtn",
52917
+ onClick: () => onTakeOver == null ? void 0 : onTakeOver(),
52918
+ className: tw`flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2 text-white shadow-lg transition hover:bg-primaryDark`,
52919
+ children: "Usar o discador nesta aba"
52920
+ }
52921
+ ),
52922
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
52923
+ "button",
52924
+ {
52925
+ type: "button",
52926
+ id: "logoutAllBtn",
52927
+ onClick: () => onLogoutAll == null ? void 0 : onLogoutAll(),
52928
+ className: tw`w-full text-center text-xs font-semibold text-red-500 transition hover:text-red-600`,
52929
+ children: "Sair em todas as abas"
52930
+ }
52931
+ ),
52932
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-[11px] text-slate-400`, children: "O Callix aceita uma conexão por usuário: você já está conectado em outra aba deste navegador e pode continuar por lá." })
52933
+ ] });
52934
+ }
52114
52935
  if (confirmation) {
52115
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: tw`space-y-4`, children: [
52936
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: tw`space-y-4`, "data-login-state": "choose", children: [
52116
52937
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: tw`space-y-2 text-center`, children: [
52117
52938
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-[11px] font-semibold uppercase tracking-wide text-slate-500`, children: "Entrar como" }),
52118
52939
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: tw`inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-sm font-semibold text-slate-800`, children: [
@@ -52121,10 +52942,12 @@ function LoginForm({ defaultValue, onSubmit, isLoading, error: error2, statusMes
52121
52942
  ] })
52122
52943
  ] }),
52123
52944
  error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-xs text-red-500`, children: error2 }),
52945
+ !error2 && confirmation.note && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-xs text-slate-500`, children: confirmation.note }),
52124
52946
  /* @__PURE__ */ jsxRuntimeExports.jsx(
52125
52947
  "button",
52126
52948
  {
52127
52949
  type: "button",
52950
+ id: "loginConfirmBtn",
52128
52951
  onClick: () => void onSubmit(confirmation.username),
52129
52952
  className: tw`flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2 text-white shadow-lg transition hover:bg-primaryDark`,
52130
52953
  children: "Entrar"
@@ -52139,7 +52962,7 @@ function LoginForm({ defaultValue, onSubmit, isLoading, error: error2, statusMes
52139
52962
  children: "Usar outro usuário"
52140
52963
  }
52141
52964
  ),
52142
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-[11px] text-slate-400`, children: "Se você já estiver conectado em outra aba, continue usando o discador por lá." })
52965
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: tw`text-center text-[11px] text-slate-400`, children: "Ao entrar, todas as abas abertas passam a usar o discador. Vai usar o Callix web? Então não entre aqui: nada é conectado sem a sua confirmação." })
52143
52966
  ] });
52144
52967
  }
52145
52968
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { className: tw`space-y-4`, onSubmit: handleSubmit, children: [
@@ -52189,19 +53012,19 @@ function AuthenticatedDialer({
52189
53012
  const isMiniMode = variant === "min";
52190
53013
  const { skipManualQualification } = useDialerConfig();
52191
53014
  console.log("[FloatingDialer/AuthenticatedDialer] 🔧 skipManualQualification:", skipManualQualification);
52192
- const operator = clientSdkReactExports.useCallOperator();
52193
- const operatorState = clientSdkReactExports.useCallOperatorState();
52194
- const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
52195
- const currentCallInfo = clientSdkReactExports.useCallOperatorCurrentCallInfo();
52196
- const rawSuccessQualifications = clientSdkReactExports.useCallOperatorSuccessQualifications();
52197
- const rawDiscardQualifications = clientSdkReactExports.useCallOperatorDiscardQualifications();
53015
+ const operator = useCallOperator();
53016
+ const operatorState = useCallOperatorState();
53017
+ const currentCall = useCallOperatorCurrentCall();
53018
+ const currentCallInfo = useCallOperatorCurrentCallInfo();
53019
+ const rawSuccessQualifications = useCallOperatorSuccessQualifications();
53020
+ const rawDiscardQualifications = useCallOperatorDiscardQualifications();
52198
53021
  const successQualifications = useMemo(() => rawSuccessQualifications ?? [], [rawSuccessQualifications]);
52199
53022
  const discardQualifications = useMemo(() => rawDiscardQualifications ?? [], [rawDiscardQualifications]);
52200
- const { makeManualCall, becomeAvailable, finishAfterCall } = clientSdkReactExports.useCallOperatorControls();
52201
- const { hangup, answer, reject } = clientSdkReactExports.useCallOperatorCurrentCallControls();
52202
- const [inputMuted, setInputMuted] = clientSdkReactExports.useCallOperatorAudioInputMuted();
52203
- const [outputMuted, setOutputMuted] = clientSdkReactExports.useCallOperatorAudioOutputMuted();
52204
- const [outputVolume, setOutputVolume] = clientSdkReactExports.useCallOperatorAudioOutputVolume();
53023
+ const { makeManualCall, becomeAvailable, finishAfterCall } = useCallOperatorControls();
53024
+ const { hangup, answer, reject } = useCallOperatorCurrentCallControls();
53025
+ const [inputMuted, setInputMuted] = useCallOperatorAudioInputMuted();
53026
+ const [outputMuted, setOutputMuted] = useCallOperatorAudioOutputMuted();
53027
+ const [outputVolume, setOutputVolume] = useCallOperatorAudioOutputVolume();
52205
53028
  useEffect(() => {
52206
53029
  console.log("[FloatingDialer] Estado do operador mudou:", {
52207
53030
  state: operatorState == null ? void 0 : operatorState.state,
@@ -52234,7 +53057,7 @@ function AuthenticatedDialer({
52234
53057
  const visibleInputRef = useRef(null);
52235
53058
  const [callError, setCallError] = useState(null);
52236
53059
  const [availabilityError, setAvailabilityError] = useState(null);
52237
- clientSdkReactExports.useCallOperatorEventListener("userSessionDropped", (data) => {
53060
+ useCallOperatorEventListener("userSessionDropped", (data) => {
52238
53061
  const notice = describeSessionDrop(data == null ? void 0 : data.originator);
52239
53062
  setAvailabilityError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
52240
53063
  });
@@ -56837,9 +57660,69 @@ function DialerNoticeBanner({ notice, onDismiss, onRecheck, onHeightChange }) {
56837
57660
  dismissible && onDismiss && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "dialer-notice-close", onClick: onDismiss, "aria-label": "Fechar aviso", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-xmark", "aria-hidden": "true" }) })
56838
57661
  ] });
56839
57662
  }
57663
+ const EMOTION_LABEL = {
57664
+ raiva: "raiva",
57665
+ irritacao: "irritação",
57666
+ frustracao: "frustração",
57667
+ desconfianca: "desconfiança",
57668
+ impaciencia: "impaciência",
57669
+ confusao: "confusão",
57670
+ nervosismo: "nervosismo",
57671
+ surpresa: "surpresa",
57672
+ desanimo: "desânimo",
57673
+ cansaco: "cansaço",
57674
+ interesse: "interesse",
57675
+ entusiasmo: "entusiasmo",
57676
+ satisfacao: "satisfação",
57677
+ alivio: "alívio",
57678
+ neutro: "neutro"
57679
+ };
57680
+ const STANCE_LABEL = {
57681
+ aberto: "cliente aberto",
57682
+ neutro: "cliente neutro",
57683
+ resistente: "cliente resistente",
57684
+ hostil: "cliente hostil",
57685
+ indefinido: "lendo a conversa"
57686
+ };
57687
+ const SPEAKER_LABEL = { agent: "Você", customer: "Cliente", mixed: "" };
57688
+ function topEmotions(segment, max2 = 2) {
57689
+ return (segment.emotions ?? []).filter((e) => e.label !== "neutro" && e.intensity >= 0.3).slice(0, max2);
57690
+ }
57691
+ function LiveCaptionPanel({ maxLines = 4 }) {
57692
+ const client2 = useEngineClient();
57693
+ if (!client2) return null;
57694
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(LiveCaptionPanelInner, { client: client2, maxLines });
57695
+ }
57696
+ function LiveCaptionPanelInner({ client: client2, maxLines }) {
57697
+ const { segments, insight, status } = useEngineTranscript(client2);
57698
+ const listening = (status == null ? void 0 : status.live) === "running";
57699
+ if (segments.length === 0 && !insight && !listening) return null;
57700
+ const rows = segments.slice(-maxLines);
57701
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "live-captions", id: "liveCaptions", "aria-live": "polite", children: [
57702
+ insight && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `live-insight stance-${insight.customer.stance}`, id: "liveInsight", title: insight.summary, children: [
57703
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "live-insight-stance", children: STANCE_LABEL[insight.customer.stance] ?? insight.customer.stance }),
57704
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "live-insight-interest", children: [
57705
+ "interesse ",
57706
+ Math.round(insight.customer.interest),
57707
+ "%"
57708
+ ] }),
57709
+ insight.nextMove && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "live-insight-next", children: insight.nextMove })
57710
+ ] }),
57711
+ rows.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "live-caption-list", children: rows.map((segment) => /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: `live-caption ${segment.speaker}${segment.interim ? " interim" : ""}${segment.refined ? " refined" : ""}`, children: [
57712
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "live-caption-speaker", children: SPEAKER_LABEL[segment.speaker] }),
57713
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "live-caption-text", children: segment.text }),
57714
+ topEmotions(segment).map((emotion) => /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `live-caption-emotion${emotion.source === "context" ? " ctx" : ""}`, title: emotion.evidence ?? "", children: [
57715
+ EMOTION_LABEL[emotion.label] ?? emotion.label,
57716
+ " ",
57717
+ Math.round(emotion.intensity * 100),
57718
+ "%"
57719
+ ] }, emotion.label))
57720
+ ] }, segment.id)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "live-caption-waiting", children: "Ouvindo a conversa…" })
57721
+ ] });
57722
+ }
56840
57723
  function useDialerNotices(options = {}) {
56841
- const operatorState = clientSdkReactExports.useCallOperatorState();
56842
- const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
57724
+ const operatorState = useCallOperatorState();
57725
+ const currentCall = useCallOperatorCurrentCall();
56843
57726
  const [deviceIssue, setDeviceIssue] = useState(null);
56844
57727
  const [sessionDrop, setSessionDrop] = useState(null);
56845
57728
  const [callFailure, setCallFailure] = useState(null);
@@ -56869,7 +57752,7 @@ function useDialerNotices(options = {}) {
56869
57752
  (_a3 = mediaDevices == null ? void 0 : mediaDevices.removeEventListener) == null ? void 0 : _a3.call(mediaDevices, "devicechange", handler);
56870
57753
  };
56871
57754
  }, [options.checkDevices]);
56872
- clientSdkReactExports.useCallOperatorEventListener("userSessionDropped", (data) => {
57755
+ useCallOperatorEventListener("userSessionDropped", (data) => {
56873
57756
  setSessionDrop((data == null ? void 0 : data.originator) ?? "unknown");
56874
57757
  });
56875
57758
  useEffect(() => {
@@ -57583,6 +58466,8 @@ function MobileDialerLogin({
57583
58466
  sessionStatus,
57584
58467
  loginConfirmation,
57585
58468
  onCancelLoginConfirmation,
58469
+ onTakeOverSession,
58470
+ onLogoutAllTabs,
57586
58471
  density
57587
58472
  }) {
57588
58473
  const [loginInput, setLoginInput] = useState(username ?? "");
@@ -57619,19 +58504,33 @@ function MobileDialerLogin({
57619
58504
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-spinner", "aria-hidden": "true" }),
57620
58505
  loadingUser && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-loading-user", children: loadingUser }),
57621
58506
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-status", children: sessionStatus || "Conectando..." })
57622
- ] }) : showConfirmation && loginConfirmation ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "login-confirm", children: [
58507
+ ] }) : showConfirmation && loginConfirmation ? loginConfirmation.elsewhere ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "login-confirm", "data-login-state": "elsewhere", children: [
58508
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-confirm-label", children: "Discador ativo em outra aba" }),
58509
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "login-user-chip", children: [
58510
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-user-check", "aria-hidden": "true" }),
58511
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: loginConfirmation.username })
58512
+ ] }),
58513
+ sessionError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "input-error", children: sessionError }),
58514
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: "login-confirm-btn login-confirm-btn-blue", id: "takeOverBtn", onClick: () => onTakeOverSession == null ? void 0 : onTakeOverSession(), children: [
58515
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-arrow-down", "aria-hidden": "true" }),
58516
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Usar o discador nesta aba" })
58517
+ ] }),
58518
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "login-secondary-btn login-secondary-danger", id: "logoutAllBtn", onClick: () => onLogoutAllTabs == null ? void 0 : onLogoutAllTabs(), children: "Sair em todas as abas" }),
58519
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "login-hint", children: "O Callix aceita uma conexão por usuário: você já está conectado em outra aba deste navegador e pode continuar por lá." })
58520
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "login-confirm", "data-login-state": "choose", children: [
57623
58521
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-confirm-label", children: "Entrar como" }),
57624
58522
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "login-user-chip", children: [
57625
58523
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-user", "aria-hidden": "true" }),
57626
58524
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: loginConfirmation.username })
57627
58525
  ] }),
57628
58526
  sessionError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "input-error", children: sessionError }),
57629
- /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: "login-confirm-btn", onClick: () => void submitLogin(loginConfirmation.username), children: [
58527
+ !sessionError && loginConfirmation.note && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "login-note", children: loginConfirmation.note }),
58528
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: "login-confirm-btn", id: "loginConfirmBtn", onClick: () => void submitLogin(loginConfirmation.username), children: [
57630
58529
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-sign-in-alt", "aria-hidden": "true" }),
57631
58530
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Entrar" })
57632
58531
  ] }),
57633
58532
  !loginConfirmation.locked && onCancelLoginConfirmation && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "login-secondary-btn", onClick: onCancelLoginConfirmation, children: "Usar outro usuário" }),
57634
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "login-hint", children: "Se você já estiver conectado em outra aba, continue usando o discador por lá." })
58533
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "login-hint", children: "Ao entrar, todas as abas abertas passam a usar o discador. Vai usar o Callix web? Então não entre aqui: nada é conectado sem a sua confirmação." })
57635
58534
  ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
57636
58535
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "phone-number-display", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
57637
58536
  "input",
@@ -57724,6 +58623,7 @@ Gostaríamos de fazer um *diagnóstico gratuito* para te apresentar os valores r
57724
58623
  }
57725
58624
  function AuthenticatedMobileDialer({
57726
58625
  username,
58626
+ onLogout,
57727
58627
  showClientInfoModal = false,
57728
58628
  density
57729
58629
  }) {
@@ -57762,21 +58662,21 @@ function AuthenticatedMobileDialer({
57762
58662
  const [speakerDeviceId, setSpeakerDeviceId] = useState(null);
57763
58663
  const [defaultDeviceId, setDefaultDeviceId] = useState("default");
57764
58664
  const [speakerSupported, setSpeakerSupported] = useState(null);
57765
- const operator = clientSdkReactExports.useCallOperator();
57766
- const operatorState = clientSdkReactExports.useCallOperatorState();
57767
- const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
57768
- const { hangup, answer, reject } = clientSdkReactExports.useCallOperatorCurrentCallControls();
57769
- const callInfo = clientSdkReactExports.useCallOperatorCurrentCallInfo();
57770
- const rawSuccessQualifications = clientSdkReactExports.useCallOperatorSuccessQualifications();
57771
- const rawDiscardQualifications = clientSdkReactExports.useCallOperatorDiscardQualifications();
58665
+ const operator = useCallOperator();
58666
+ const operatorState = useCallOperatorState();
58667
+ const currentCall = useCallOperatorCurrentCall();
58668
+ const { hangup, answer, reject } = useCallOperatorCurrentCallControls();
58669
+ const callInfo = useCallOperatorCurrentCallInfo();
58670
+ const rawSuccessQualifications = useCallOperatorSuccessQualifications();
58671
+ const rawDiscardQualifications = useCallOperatorDiscardQualifications();
57772
58672
  const successQualifications = useMemo(() => rawSuccessQualifications ?? [], [rawSuccessQualifications]);
57773
58673
  const discardQualifications = useMemo(() => rawDiscardQualifications ?? [], [rawDiscardQualifications]);
57774
- const { makeManualCall, finishAfterCall } = clientSdkReactExports.useCallOperatorControls();
58674
+ const { makeManualCall, finishAfterCall } = useCallOperatorControls();
57775
58675
  const { goOnBreak, goOffBreak } = useOperatorBreak();
57776
58676
  const breaks = useSignal(operator == null ? void 0 : operator.breaks) || [];
57777
58677
  const userData = useSignal(operator == null ? void 0 : operator.user);
57778
58678
  const isOnBreak = (operatorState == null ? void 0 : operatorState.state) === "onBreak";
57779
- const [inputMuted, setInputMuted] = clientSdkReactExports.useCallOperatorAudioInputMuted();
58679
+ const [inputMuted, setInputMuted] = useCallOperatorAudioInputMuted();
57780
58680
  const haptic = useHaptic();
57781
58681
  const dtmf = useDTMF({ enabled: true });
57782
58682
  const keySequenceRef = useRef("");
@@ -58490,13 +59390,30 @@ function AuthenticatedMobileDialer({
58490
59390
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "pause-timer", children: pauseDurationLabel })
58491
59391
  ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "status-text", children: statusBadgeText })
58492
59392
  ] }) }),
58493
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "status-actions", children: isOnBreak ? /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "resume-btn", id: "resumeBtn", onClick: handleGoOffBreak, children: [
58494
- /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-play" }),
58495
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Voltar" })
58496
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "pause-btn", id: "pauseBtn", onClick: () => setIsPauseModalOpen(true), children: [
58497
- /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-pause" }),
58498
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pausa" })
58499
- ] }) })
59393
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "status-actions", children: [
59394
+ isOnBreak ? /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "resume-btn", id: "resumeBtn", onClick: handleGoOffBreak, children: [
59395
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-play" }),
59396
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Voltar" })
59397
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "pause-btn", id: "pauseBtn", onClick: () => setIsPauseModalOpen(true), children: [
59398
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-pause" }),
59399
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pausa" })
59400
+ ] }),
59401
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
59402
+ "button",
59403
+ {
59404
+ type: "button",
59405
+ className: "logout-btn",
59406
+ id: "logoutBtn",
59407
+ title: "Sair do discador (todas as abas)",
59408
+ "aria-label": "Sair do discador",
59409
+ onClick: () => {
59410
+ if (typeof window !== "undefined" && !window.confirm("Sair do discador em todas as abas?")) return;
59411
+ onLogout();
59412
+ },
59413
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-right-from-bracket" })
59414
+ }
59415
+ )
59416
+ ] })
58500
59417
  ] }),
58501
59418
  /* @__PURE__ */ jsxRuntimeExports.jsx(DialerNoticeBanner, { notice: dialerNotice, onDismiss: dismissDialerNotice, onRecheck: () => void recheckDevices(), onHeightChange: setNoticeHeight }),
58502
59419
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "phone-number-display", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -58595,6 +59512,7 @@ function AuthenticatedMobileDialer({
58595
59512
  ] })
58596
59513
  ] }) })
58597
59514
  ] }),
59515
+ /* @__PURE__ */ jsxRuntimeExports.jsx(LiveCaptionPanel, {}),
58598
59516
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "call-controls", children: [
58599
59517
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "control-row", children: [
58600
59518
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -59049,14 +59967,14 @@ function AuthenticatedMiniDialer({
59049
59967
  }) {
59050
59968
  var _a2;
59051
59969
  const { skipManualQualification } = useDialerConfig();
59052
- const operatorState = clientSdkReactExports.useCallOperatorState();
59053
- const currentCallInfo = clientSdkReactExports.useCallOperatorCurrentCallInfo();
59054
- const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
59055
- const rawSuccessQualifications = clientSdkReactExports.useCallOperatorSuccessQualifications();
59056
- const rawDiscardQualifications = clientSdkReactExports.useCallOperatorDiscardQualifications();
59970
+ const operatorState = useCallOperatorState();
59971
+ const currentCallInfo = useCallOperatorCurrentCallInfo();
59972
+ const currentCall = useCallOperatorCurrentCall();
59973
+ const rawSuccessQualifications = useCallOperatorSuccessQualifications();
59974
+ const rawDiscardQualifications = useCallOperatorDiscardQualifications();
59057
59975
  const successQualifications = useMemo(() => rawSuccessQualifications ?? [], [rawSuccessQualifications]);
59058
59976
  const discardQualifications = useMemo(() => rawDiscardQualifications ?? [], [rawDiscardQualifications]);
59059
- const { finishAfterCall } = clientSdkReactExports.useCallOperatorControls();
59977
+ const { finishAfterCall } = useCallOperatorControls();
59060
59978
  const [isClientInfoModalOpen, setIsClientInfoModalOpen] = useState(false);
59061
59979
  const [isQualificationModalOpen, setIsQualificationModalOpen] = useState(false);
59062
59980
  const [callDurationSeconds, setCallDurationSeconds] = useState(0);
@@ -59331,6 +60249,206 @@ function AuthenticatedMiniDialer({
59331
60249
  )
59332
60250
  ] });
59333
60251
  }
60252
+ const PROBE_TIMEOUT_MS = 600;
60253
+ const TAKEOVER_GRACE_MS = 3e3;
60254
+ const TAKEOVER_JITTER_MS = 600;
60255
+ const LEADER_HEARTBEAT_MS = 3e3;
60256
+ const LEADER_STALE_MS = 9e3;
60257
+ function channelName(username) {
60258
+ return `callix-dialer:${username.trim().toLowerCase()}`;
60259
+ }
60260
+ function leaderKey(username) {
60261
+ return `callix_dialer_${username}_leader`;
60262
+ }
60263
+ const TAB_ROLE_KEY = "callix_dialer_tab_role";
60264
+ const BUS_FALLBACK_SUFFIX = "_bus";
60265
+ function newTabId() {
60266
+ const cryptoApi = typeof globalThis.crypto !== "undefined" ? globalThis.crypto : void 0;
60267
+ if (cryptoApi && "randomUUID" in cryptoApi) return cryptoApi.randomUUID().slice(0, 8);
60268
+ return Math.random().toString(36).slice(2, 10);
60269
+ }
60270
+ function defaultChannelFactory(name) {
60271
+ if (typeof BroadcastChannel === "undefined") return null;
60272
+ try {
60273
+ return new BroadcastChannel(name);
60274
+ } catch {
60275
+ return null;
60276
+ }
60277
+ }
60278
+ function defaultStorage() {
60279
+ try {
60280
+ return typeof localStorage !== "undefined" ? localStorage : null;
60281
+ } catch {
60282
+ return null;
60283
+ }
60284
+ }
60285
+ class DialerTabBus {
60286
+ constructor(options) {
60287
+ this.listeners = /* @__PURE__ */ new Set();
60288
+ this.storageListener = null;
60289
+ this.closed = false;
60290
+ this.username = options.username;
60291
+ this.tabId = options.tabId ?? newTabId();
60292
+ this.status = options.status;
60293
+ this.storage = options.storage === void 0 ? defaultStorage() : options.storage;
60294
+ const factory = options.channelFactory ?? defaultChannelFactory;
60295
+ this.channel = factory(channelName(this.username));
60296
+ if (this.channel) {
60297
+ this.channel.onmessage = (event) => this.receive(event.data);
60298
+ } else if (typeof window !== "undefined" && this.storage) {
60299
+ const key = this.fallbackKey();
60300
+ this.storageListener = (event) => {
60301
+ if (event.key !== key || !event.newValue) return;
60302
+ try {
60303
+ const parsed = JSON.parse(event.newValue);
60304
+ this.receive(parsed.message);
60305
+ } catch {
60306
+ }
60307
+ };
60308
+ window.addEventListener("storage", this.storageListener);
60309
+ }
60310
+ }
60311
+ fallbackKey() {
60312
+ return `callix_dialer_${this.username}${BUS_FALLBACK_SUFFIX}`;
60313
+ }
60314
+ on(listener) {
60315
+ this.listeners.add(listener);
60316
+ return () => {
60317
+ this.listeners.delete(listener);
60318
+ };
60319
+ }
60320
+ post(message) {
60321
+ if (this.closed) return;
60322
+ const full = { ...message, from: this.tabId };
60323
+ if (this.channel) {
60324
+ try {
60325
+ this.channel.postMessage(full);
60326
+ } catch {
60327
+ }
60328
+ return;
60329
+ }
60330
+ if (this.storage) {
60331
+ try {
60332
+ this.storage.setItem(this.fallbackKey(), JSON.stringify({ nonce: Math.random(), at: Date.now(), message: full }));
60333
+ } catch {
60334
+ }
60335
+ }
60336
+ }
60337
+ receive(data) {
60338
+ if (this.closed || !data || typeof data !== "object") return;
60339
+ const message = data;
60340
+ if (typeof message.type !== "string" || typeof message.from !== "string" || message.from === this.tabId) return;
60341
+ if (message.type === "ping") {
60342
+ const status = this.status();
60343
+ this.post({ type: "pong", role: status.role, authenticated: status.authenticated });
60344
+ }
60345
+ for (const listener of this.listeners) listener(message);
60346
+ }
60347
+ /** pergunta quem está aí; devolve as abas que responderam dentro do prazo */
60348
+ probe(timeoutMs = PROBE_TIMEOUT_MS) {
60349
+ return new Promise((resolve) => {
60350
+ const found = /* @__PURE__ */ new Map();
60351
+ const off = this.on((message) => {
60352
+ if (message.type === "pong") found.set(message.from, { tabId: message.from, role: message.role, authenticated: message.authenticated });
60353
+ });
60354
+ this.post({ type: "ping" });
60355
+ setTimeout(() => {
60356
+ off();
60357
+ resolve([...found.values()]);
60358
+ }, timeoutMs);
60359
+ });
60360
+ }
60361
+ close() {
60362
+ this.closed = true;
60363
+ this.listeners.clear();
60364
+ if (this.channel) {
60365
+ this.channel.onmessage = null;
60366
+ try {
60367
+ this.channel.close();
60368
+ } catch {
60369
+ }
60370
+ }
60371
+ if (this.storageListener && typeof window !== "undefined") {
60372
+ window.removeEventListener("storage", this.storageListener);
60373
+ this.storageListener = null;
60374
+ }
60375
+ }
60376
+ }
60377
+ function findLeader(pongs) {
60378
+ return pongs.find((pong) => pong.role === "leader" && pong.authenticated) ?? null;
60379
+ }
60380
+ function anyDialerTab(pongs) {
60381
+ return pongs.some((pong) => pong.role !== null);
60382
+ }
60383
+ function readLeader(username, storage = defaultStorage(), now = Date.now()) {
60384
+ if (!storage) return null;
60385
+ try {
60386
+ const raw = storage.getItem(leaderKey(username));
60387
+ if (!raw) return null;
60388
+ const parsed = JSON.parse(raw);
60389
+ if (typeof parsed.tabId !== "string" || typeof parsed.at !== "number") return null;
60390
+ if (now - parsed.at > LEADER_STALE_MS) return null;
60391
+ return { tabId: parsed.tabId, at: parsed.at };
60392
+ } catch {
60393
+ return null;
60394
+ }
60395
+ }
60396
+ function claimLeader(username, tabId, storage = defaultStorage(), now = Date.now()) {
60397
+ if (!storage) return true;
60398
+ const current = readLeader(username, storage, now);
60399
+ if (current && current.tabId !== tabId) return false;
60400
+ try {
60401
+ storage.setItem(leaderKey(username), JSON.stringify({ tabId, at: now }));
60402
+ } catch {
60403
+ }
60404
+ return true;
60405
+ }
60406
+ function releaseLeader(username, tabId, storage = defaultStorage()) {
60407
+ if (!storage) return;
60408
+ try {
60409
+ const raw = storage.getItem(leaderKey(username));
60410
+ if (!raw) return;
60411
+ const parsed = JSON.parse(raw);
60412
+ if (parsed.tabId === tabId) storage.removeItem(leaderKey(username));
60413
+ } catch {
60414
+ }
60415
+ }
60416
+ function defaultSessionStorage() {
60417
+ try {
60418
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
60419
+ } catch {
60420
+ return null;
60421
+ }
60422
+ }
60423
+ function saveTabRole(username, role, storage = defaultSessionStorage()) {
60424
+ if (!storage) return;
60425
+ try {
60426
+ storage.setItem(TAB_ROLE_KEY, JSON.stringify({ username, role, at: Date.now() }));
60427
+ } catch {
60428
+ }
60429
+ }
60430
+ function readTabRole(username, storage = defaultSessionStorage()) {
60431
+ if (!storage) return null;
60432
+ try {
60433
+ const raw = storage.getItem(TAB_ROLE_KEY);
60434
+ if (!raw) return null;
60435
+ const parsed = JSON.parse(raw);
60436
+ if (parsed.username !== username) return null;
60437
+ return parsed.role === "leader" || parsed.role === "member" ? parsed.role : null;
60438
+ } catch {
60439
+ return null;
60440
+ }
60441
+ }
60442
+ function clearTabRole(storage = defaultSessionStorage()) {
60443
+ if (!storage) return;
60444
+ try {
60445
+ storage.removeItem(TAB_ROLE_KEY);
60446
+ } catch {
60447
+ }
60448
+ }
60449
+ function takeoverDelayMs(random = Math.random()) {
60450
+ return TAKEOVER_GRACE_MS + Math.floor(random * TAKEOVER_JITTER_MS);
60451
+ }
59334
60452
  const DEFAULT_CLICK_TO_CALL_INTERVAL_MS = 4e3;
59335
60453
  const BUSY_INTERVAL_FACTOR = 3;
59336
60454
  const DISPATCH_COOLDOWN_MS = 6e3;
@@ -59350,8 +60468,8 @@ function buildHeaders(sessionToken) {
59350
60468
  }
59351
60469
  const delay = (ms2) => new Promise((resolve) => setTimeout(resolve, ms2));
59352
60470
  function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask }) {
59353
- const operatorState = clientSdkReactExports.useCallOperatorState();
59354
- const { makeManualCall } = clientSdkReactExports.useCallOperatorControls();
60471
+ const operatorState = useCallOperatorState();
60472
+ const { makeManualCall } = useCallOperatorControls();
59355
60473
  const stateRef = useRef(operatorState == null ? void 0 : operatorState.state);
59356
60474
  stateRef.current = operatorState == null ? void 0 : operatorState.state;
59357
60475
  const makeManualCallRef = useRef(makeManualCall);
@@ -59553,7 +60671,7 @@ function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask
59553
60671
  }, [enabled, pollIntervalMs, sessionToken]);
59554
60672
  return null;
59555
60673
  }
59556
- const WIDGET_VERSION = "2.5.1";
60674
+ const WIDGET_VERSION = "2.7.0";
59557
60675
  let currentSipWebSocket = null;
59558
60676
  function sendSipUnregisterIfPossible(ws) {
59559
60677
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -59679,9 +60797,23 @@ function useDialerConfig() {
59679
60797
  return context;
59680
60798
  }
59681
60799
  const SdkCleanupManager = forwardRef(
59682
- function SdkCleanupManager2({ children: children2 }, ref) {
59683
- const operator = clientSdkReactExports.useCallOperator();
59684
- const controls = clientSdkReactExports.useCallOperatorControls();
60800
+ function SdkCleanupManager2({ children: children2, onSessionDropped }, ref) {
60801
+ const operator = useCallOperator();
60802
+ const controls = useCallOperatorControls();
60803
+ const operatorState = useCallOperatorState();
60804
+ const droppedRef = useRef(onSessionDropped);
60805
+ droppedRef.current = onSessionDropped;
60806
+ useCallOperatorEventListener("userSessionDropped", (data) => {
60807
+ var _a2;
60808
+ const originator = (data == null ? void 0 : data.originator) ?? null;
60809
+ (_a2 = droppedRef.current) == null ? void 0 : _a2.call(droppedRef, originator, describeSessionDrop(originator));
60810
+ });
60811
+ const startupError = (operatorState == null ? void 0 : operatorState.state) === "startupError" ? operatorState.error ?? null : null;
60812
+ useEffect(() => {
60813
+ var _a2;
60814
+ if (startupError !== "userIsAlreadyConnectedOnAnotherDevice") return;
60815
+ (_a2 = droppedRef.current) == null ? void 0 : _a2.call(droppedRef, "anotherDevice", describeStartupError(startupError));
60816
+ }, [startupError]);
59685
60817
  useImperativeHandle(ref, () => ({
59686
60818
  goOffline: () => {
59687
60819
  console.log("[Callix Dialer] 📴 Chamando goOffline() para enviar SIP UNREGISTER...");
@@ -59702,21 +60834,27 @@ const SdkCleanupManager = forwardRef(
59702
60834
  }
59703
60835
  }
59704
60836
  }), [operator, controls]);
60837
+ const controlsRef = useRef(controls);
60838
+ controlsRef.current = controls;
59705
60839
  useEffect(() => {
59706
60840
  return () => {
59707
60841
  console.log("[Callix Dialer] 🔌 SdkCleanupManager desmontando, chamando goOffline()...");
59708
60842
  try {
59709
- controls.goOffline();
60843
+ controlsRef.current.goOffline();
59710
60844
  } catch (err) {
59711
60845
  console.warn("[Callix Dialer] ⚠️ Erro no cleanup automático:", err);
59712
60846
  }
59713
60847
  };
59714
- }, [controls]);
60848
+ }, []);
59715
60849
  return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: children2 });
59716
60850
  }
59717
60851
  );
59718
60852
  function deriveOptionsFromElement(element) {
59719
60853
  const result2 = {};
60854
+ const datasetEngineUrl = element.getAttribute("data-callix-engine-url") ?? element.dataset.callixEngineUrl;
60855
+ if (datasetEngineUrl && datasetEngineUrl.trim()) result2.engineUrl = datasetEngineUrl.trim();
60856
+ const datasetEngineToken = element.getAttribute("data-callix-engine-token") ?? element.dataset.callixEngineToken;
60857
+ if (datasetEngineToken && datasetEngineToken.trim()) result2.engineToken = datasetEngineToken.trim();
59720
60858
  const datasetMode = element.getAttribute("data-callix-mode") || element.dataset.callixMode || element.dataset.mode;
59721
60859
  const datasetUsername = element.getAttribute("data-callix-username") || element.dataset.callixUsername || element.dataset.username;
59722
60860
  const classList = Array.from(element.classList);
@@ -59808,6 +60946,12 @@ class CallixDialerWidget {
59808
60946
  if (this.options.clickToCall === void 0 && derived.clickToCall !== void 0) {
59809
60947
  this.options.clickToCall = derived.clickToCall;
59810
60948
  }
60949
+ if (!this.options.engineUrl && derived.engineUrl) {
60950
+ this.options.engineUrl = derived.engineUrl;
60951
+ }
60952
+ if (!this.options.engineToken && derived.engineToken) {
60953
+ this.options.engineToken = derived.engineToken;
60954
+ }
59811
60955
  this.root = clientExports.createRoot(this.container);
59812
60956
  this.root.render(
59813
60957
  /* @__PURE__ */ jsxRuntimeExports.jsx(StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(DialerWrapper, { options: this.options, containerEl: this.container }) })
@@ -59840,6 +60984,27 @@ const getStorageKeys = (username) => ({
59840
60984
  SKIP_MANUAL_QUALIFICATION: `callix_dialer_${username}_skip_manual_qualification`
59841
60985
  });
59842
60986
  const LAST_USERNAME_KEY = "callix_dialer_last_username";
60987
+ function readStoredToken(user) {
60988
+ try {
60989
+ return localStorage.getItem(getStorageKeys(user).SESSION_TOKEN);
60990
+ } catch {
60991
+ return null;
60992
+ }
60993
+ }
60994
+ async function fetchSkipQualification(domain, token) {
60995
+ var _a2, _b;
60996
+ try {
60997
+ const response = await fetch(`https://${domain}/sdk/v1/client/user-service/config`, {
60998
+ headers: { Authorization: `Bearer ${token}` },
60999
+ credentials: "omit"
61000
+ });
61001
+ if (!response.ok) return null;
61002
+ const data = await response.json();
61003
+ return ((_b = (_a2 = data == null ? void 0 : data.service) == null ? void 0 : _a2.outboundOptions) == null ? void 0 : _b.skipQualification) === true;
61004
+ } catch {
61005
+ return null;
61006
+ }
61007
+ }
59843
61008
  function DialerWrapper({ options, containerEl }) {
59844
61009
  const [username, setUsername] = useState(null);
59845
61010
  const [isAuthenticated, setIsAuthenticated] = useState(false);
@@ -59855,10 +61020,35 @@ function DialerWrapper({ options, containerEl }) {
59855
61020
  const [isDisconnecting, setIsDisconnecting] = useState(false);
59856
61021
  const presetUsername = options.presetUsername ? options.presetUsername.trim() : null;
59857
61022
  const uiVariant = options.uiVariant ?? "default";
59858
- const autoLogin = options.autoLogin === true;
59859
61023
  const portalTarget = useMemo(() => options.portalTarget ?? containerEl ?? null, [options.portalTarget, containerEl]);
59860
- const autoLoginAttemptedRef = useRef(false);
59861
- const lastPresetRef = useRef(presetUsername);
61024
+ const tabIdRef = useRef("");
61025
+ if (!tabIdRef.current) tabIdRef.current = newTabId();
61026
+ const [tabRole, setTabRole] = useState(null);
61027
+ const tabRoleRef = useRef(null);
61028
+ tabRoleRef.current = tabRole;
61029
+ const [tabDecided, setTabDecided] = useState(false);
61030
+ const [tabNote, setTabNote] = useState(null);
61031
+ const [engineWaiting, setEngineWaiting] = useState(false);
61032
+ const busRef = useRef(null);
61033
+ const takeoverTimerRef = useRef(null);
61034
+ const isAuthenticatedRef = useRef(false);
61035
+ isAuthenticatedRef.current = isAuthenticated;
61036
+ const isLoadingRef = useRef(false);
61037
+ isLoadingRef.current = isLoading;
61038
+ const logoffPendingRef = useRef(false);
61039
+ const handleLoginRef = useRef(async () => void 0);
61040
+ const handleLogoutRef = useRef(async () => void 0);
61041
+ const cancelTakeover = useCallback(() => {
61042
+ if (takeoverTimerRef.current) {
61043
+ clearTimeout(takeoverTimerRef.current);
61044
+ takeoverTimerRef.current = null;
61045
+ }
61046
+ }, []);
61047
+ useEffect(() => {
61048
+ if (options.autoLogin) {
61049
+ console.info("[Callix Dialer] autoLogin: desde a 2.7.0 o widget não cria sessão sem confirmação; ele entra sozinho só quando já existe uma conexão ativa deste usuário (outra aba ou o motor).");
61050
+ }
61051
+ }, [options.autoLogin]);
59862
61052
  const currentSessionRef = useRef({
59863
61053
  apiUrl: options.apiUrl || "/api/user-sdk-session",
59864
61054
  token: null
@@ -59866,9 +61056,60 @@ function DialerWrapper({ options, containerEl }) {
59866
61056
  useEffect(() => {
59867
61057
  currentSessionRef.current.token = sessionToken;
59868
61058
  }, [sessionToken]);
61059
+ const engineMode = Boolean(options.engineUrl);
61060
+ const engineModeRef = useRef(engineMode);
61061
+ engineModeRef.current = engineMode;
61062
+ const engineRef = useRef(null);
61063
+ const ensureEngine = useCallback(
61064
+ (user) => {
61065
+ const current = engineRef.current;
61066
+ if (current && current.agentId === user) {
61067
+ current.connect();
61068
+ return current;
61069
+ }
61070
+ current == null ? void 0 : current.close();
61071
+ const client2 = new EngineClient({
61072
+ url: options.engineUrl ?? "",
61073
+ token: options.engineToken ?? "",
61074
+ agentId: user,
61075
+ label: "Discador",
61076
+ log: (message) => console.log("[Callix Dialer/motor]", message)
61077
+ });
61078
+ client2.onSessionLost((lost) => {
61079
+ const notice = describeSessionDrop(lost.originator);
61080
+ setError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
61081
+ setIsAuthenticated(false);
61082
+ setSessionToken(null);
61083
+ setTabNote(null);
61084
+ });
61085
+ client2.connect();
61086
+ engineRef.current = client2;
61087
+ return client2;
61088
+ },
61089
+ [options.engineUrl, options.engineToken]
61090
+ );
61091
+ useEffect(
61092
+ () => () => {
61093
+ var _a2;
61094
+ (_a2 = engineRef.current) == null ? void 0 : _a2.close();
61095
+ engineRef.current = null;
61096
+ },
61097
+ []
61098
+ );
59869
61099
  useEffect(() => {
61100
+ let unloadHandled = false;
59870
61101
  const handleBeforeUnload = () => {
61102
+ var _a2;
61103
+ if (engineModeRef.current || unloadHandled) return;
61104
+ unloadHandled = true;
61105
+ setTimeout(() => {
61106
+ unloadHandled = false;
61107
+ }, 2e3);
59871
61108
  const { apiUrl, token } = currentSessionRef.current;
61109
+ if (tabRoleRef.current === "leader") {
61110
+ (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "leaving" });
61111
+ if (busRef.current) releaseLeader(busRef.current.username, tabIdRef.current);
61112
+ }
59872
61113
  if (token) {
59873
61114
  const data = JSON.stringify({ logout: true });
59874
61115
  const logoutUrl = `${apiUrl}?logout=true`;
@@ -59887,8 +61128,10 @@ function DialerWrapper({ options, containerEl }) {
59887
61128
  }
59888
61129
  };
59889
61130
  window.addEventListener("beforeunload", handleBeforeUnload);
61131
+ window.addEventListener("pagehide", handleBeforeUnload);
59890
61132
  return () => {
59891
61133
  window.removeEventListener("beforeunload", handleBeforeUnload);
61134
+ window.removeEventListener("pagehide", handleBeforeUnload);
59892
61135
  };
59893
61136
  }, []);
59894
61137
  useEffect(() => {
@@ -59926,12 +61169,300 @@ function DialerWrapper({ options, containerEl }) {
59926
61169
  setUsername(presetUsername);
59927
61170
  }
59928
61171
  }, [presetUsername, username]);
61172
+ const engineDomain = useCallback(
61173
+ () => options.domain || new URL(options.apiUrl || "/api/user-sdk-session", window.location.href).host,
61174
+ [options.domain, options.apiUrl]
61175
+ );
61176
+ const enterEngine = useCallback(async (user, domain, token) => {
61177
+ if (token) {
61178
+ const skip = await fetchSkipQualification(domain, token);
61179
+ if (skip !== null) {
61180
+ setSkipManualQualification(skip);
61181
+ try {
61182
+ localStorage.setItem(getStorageKeys(user).SKIP_MANUAL_QUALIFICATION, String(skip));
61183
+ } catch {
61184
+ }
61185
+ }
61186
+ }
61187
+ try {
61188
+ localStorage.setItem(LAST_USERNAME_KEY, user);
61189
+ if (token) {
61190
+ const STORAGE_KEYS = getStorageKeys(user);
61191
+ localStorage.setItem(STORAGE_KEYS.SESSION_TOKEN, token);
61192
+ localStorage.setItem(STORAGE_KEYS.DOMAIN, domain);
61193
+ localStorage.setItem(STORAGE_KEYS.TIMESTAMP, Date.now().toString());
61194
+ }
61195
+ } catch {
61196
+ }
61197
+ setSessionToken(token);
61198
+ setCallixDomain(domain);
61199
+ setUsername(user);
61200
+ setIsAuthenticated(true);
61201
+ setError(null);
61202
+ setTabNote(null);
61203
+ setProviderKey(Date.now());
61204
+ }, []);
61205
+ const joinEngine = useCallback(
61206
+ async (user) => {
61207
+ setIsLoading(true);
61208
+ setError(null);
61209
+ setLoginStatus("Entrando na sessão do motor...");
61210
+ try {
61211
+ await enterEngine(user, engineDomain(), readStoredToken(user));
61212
+ console.log("[Callix Dialer] Conectado ao motor como", user);
61213
+ } catch (err) {
61214
+ console.error("[Callix Dialer] Erro ao entrar na sessão do motor:", err);
61215
+ setError(err instanceof Error ? err.message : "Erro ao conectar ao motor");
61216
+ } finally {
61217
+ setIsLoading(false);
61218
+ setLoginStatus(null);
61219
+ }
61220
+ },
61221
+ [enterEngine, engineDomain]
61222
+ );
61223
+ useEffect(() => {
61224
+ if (!engineMode || !username || isRestoringSession) return;
61225
+ const user = username;
61226
+ const client2 = ensureEngine(user);
61227
+ setEngineWaiting(!client2.getState().view);
61228
+ const fallback = setTimeout(() => setEngineWaiting(false), 15e3);
61229
+ let joining = false;
61230
+ const sync = () => {
61231
+ const { view, sessionLost } = client2.getState();
61232
+ if (!view) return;
61233
+ setEngineWaiting(false);
61234
+ if (view.engine === "stopped" || view.engine === "crashed") logoffPendingRef.current = false;
61235
+ const hasSession = (view.engine === "running" || view.engine === "starting") && !sessionLost;
61236
+ if (hasSession && !logoffPendingRef.current && !isAuthenticatedRef.current && !isLoadingRef.current && !joining) {
61237
+ joining = true;
61238
+ console.log("[Callix Dialer] o motor já segura a sessão deste agente; entrando por aqui também");
61239
+ void joinEngine(user).finally(() => {
61240
+ joining = false;
61241
+ });
61242
+ } else if (view.engine === "stopped" && isAuthenticatedRef.current && !isLoadingRef.current) {
61243
+ console.log("[Callix Dialer] o motor desligou (Sair em outra aba ou servidor reiniciado); voltando à tela de login");
61244
+ setIsAuthenticated(false);
61245
+ setSessionToken(null);
61246
+ setCallixDomain(null);
61247
+ setTabNote("O discador foi encerrado em outra aba ou o motor foi reiniciado.");
61248
+ }
61249
+ };
61250
+ sync();
61251
+ const off = client2.subscribe(sync);
61252
+ return () => {
61253
+ off();
61254
+ clearTimeout(fallback);
61255
+ };
61256
+ }, [engineMode, username, isRestoringSession, ensureEngine, joinEngine]);
61257
+ const scheduleTakeoverRef = useRef(null);
61258
+ useEffect(() => {
61259
+ if (engineMode || !username) {
61260
+ setTabDecided(true);
61261
+ return;
61262
+ }
61263
+ setTabDecided(false);
61264
+ const user = username;
61265
+ const bus = new DialerTabBus({
61266
+ username: user,
61267
+ tabId: tabIdRef.current,
61268
+ status: () => ({ role: tabRoleRef.current, authenticated: isAuthenticatedRef.current })
61269
+ });
61270
+ busRef.current = bus;
61271
+ const becomeMember = () => {
61272
+ if (isAuthenticatedRef.current || tabRoleRef.current === "leader") return;
61273
+ setTabRole("member");
61274
+ saveTabRole(user, "member");
61275
+ setTabNote(null);
61276
+ setError(null);
61277
+ setTabDecided(true);
61278
+ };
61279
+ const scheduleTakeover = () => {
61280
+ cancelTakeover();
61281
+ takeoverTimerRef.current = setTimeout(() => {
61282
+ takeoverTimerRef.current = null;
61283
+ if (tabRoleRef.current !== "member" || isAuthenticatedRef.current || isLoadingRef.current) return;
61284
+ const current = readLeader(user);
61285
+ if (current && current.tabId !== tabIdRef.current) return;
61286
+ if (!claimLeader(user, tabIdRef.current)) return;
61287
+ bus.post({ type: "claim" });
61288
+ console.log("[Callix Dialer] a aba com o discador fechou; esta aba assume a conexão");
61289
+ void handleLoginRef.current(user);
61290
+ }, takeoverDelayMs());
61291
+ };
61292
+ scheduleTakeoverRef.current = scheduleTakeover;
61293
+ const off = bus.on((message) => {
61294
+ switch (message.type) {
61295
+ case "login":
61296
+ case "claim":
61297
+ cancelTakeover();
61298
+ becomeMember();
61299
+ break;
61300
+ case "leaving":
61301
+ if (tabRoleRef.current === "member" && !isAuthenticatedRef.current) scheduleTakeover();
61302
+ break;
61303
+ case "takeover":
61304
+ if (tabRoleRef.current === "leader") {
61305
+ console.log("[Callix Dialer] outra aba pediu o discador; soltando a conexão");
61306
+ void handleLogoutRef.current({ broadcast: false, keepDecision: true });
61307
+ }
61308
+ break;
61309
+ case "logout":
61310
+ cancelTakeover();
61311
+ void handleLogoutRef.current({ broadcast: false });
61312
+ setTabNote("Você saiu do discador em outra aba.");
61313
+ break;
61314
+ case "dropped": {
61315
+ cancelTakeover();
61316
+ releaseLeader(user, tabIdRef.current);
61317
+ setTabRole(null);
61318
+ clearTabRole();
61319
+ const notice = describeSessionDrop(message.originator);
61320
+ setError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
61321
+ break;
61322
+ }
61323
+ }
61324
+ });
61325
+ return () => {
61326
+ off();
61327
+ bus.close();
61328
+ cancelTakeover();
61329
+ scheduleTakeoverRef.current = null;
61330
+ if (busRef.current === bus) busRef.current = null;
61331
+ setTabDecided(false);
61332
+ };
61333
+ }, [engineMode, username, cancelTakeover]);
61334
+ useEffect(() => {
61335
+ if (engineMode || !username || isAuthenticated || tabDecided || isRestoringSession || isLoading) return;
61336
+ const bus = busRef.current;
61337
+ if (!bus) return;
61338
+ const user = username;
61339
+ const stored = readTabRole(user);
61340
+ if (stored === "leader") {
61341
+ cancelTakeover();
61342
+ setTabDecided(true);
61343
+ if (claimLeader(user, tabIdRef.current)) {
61344
+ bus.post({ type: "claim" });
61345
+ console.log("[Callix Dialer] esta aba segurava o discador antes de recarregar; reconectando");
61346
+ void handleLoginRef.current(user);
61347
+ } else {
61348
+ setTabRole("member");
61349
+ saveTabRole(user, "member");
61350
+ }
61351
+ return;
61352
+ }
61353
+ let cancelled = false;
61354
+ void bus.probe().then((pongs) => {
61355
+ var _a2;
61356
+ if (cancelled) return;
61357
+ setTabDecided(true);
61358
+ const leaderLock = readLeader(user);
61359
+ if (findLeader(pongs) || leaderLock && leaderLock.tabId !== tabIdRef.current || stored === "member" && anyDialerTab(pongs)) {
61360
+ setTabRole("member");
61361
+ saveTabRole(user, "member");
61362
+ } else if (stored === "member") {
61363
+ setTabRole("member");
61364
+ (_a2 = scheduleTakeoverRef.current) == null ? void 0 : _a2.call(scheduleTakeoverRef);
61365
+ } else {
61366
+ setTabRole(null);
61367
+ clearTabRole();
61368
+ }
61369
+ });
61370
+ return () => {
61371
+ cancelled = true;
61372
+ };
61373
+ }, [engineMode, username, isAuthenticated, tabDecided, isRestoringSession, isLoading, cancelTakeover]);
61374
+ useEffect(() => {
61375
+ if (engineMode || tabRole !== "leader" || !username) return;
61376
+ const user = username;
61377
+ claimLeader(user, tabIdRef.current);
61378
+ const timer = setInterval(() => claimLeader(user, tabIdRef.current), LEADER_HEARTBEAT_MS);
61379
+ return () => clearInterval(timer);
61380
+ }, [engineMode, tabRole, username]);
61381
+ useEffect(() => {
61382
+ if (engineMode || tabRole !== "member" || !username) return;
61383
+ const user = username;
61384
+ const check = () => {
61385
+ if (document.visibilityState !== "visible" || takeoverTimerRef.current) return;
61386
+ const bus = busRef.current;
61387
+ if (!bus) return;
61388
+ void bus.probe().then((pongs) => {
61389
+ var _a2;
61390
+ if (tabRoleRef.current !== "member" || isAuthenticatedRef.current || takeoverTimerRef.current) return;
61391
+ if (findLeader(pongs) || readLeader(user)) return;
61392
+ console.log("[Callix Dialer] a aba com o discador não responde; esta aba vai assumir");
61393
+ (_a2 = scheduleTakeoverRef.current) == null ? void 0 : _a2.call(scheduleTakeoverRef);
61394
+ });
61395
+ };
61396
+ document.addEventListener("visibilitychange", check);
61397
+ window.addEventListener("focus", check);
61398
+ return () => {
61399
+ document.removeEventListener("visibilitychange", check);
61400
+ window.removeEventListener("focus", check);
61401
+ };
61402
+ }, [engineMode, tabRole, username]);
59929
61403
  const handleLogin = useCallback(async (user) => {
59930
- var _a2, _b;
61404
+ var _a2, _b, _c, _d;
59931
61405
  setIsLoading(true);
59932
61406
  setError(null);
59933
61407
  setLoginStatus("Preparando login...");
59934
61408
  console.log("[Callix Dialer] 🚀 handleLogin iniciado para usuário:", user);
61409
+ if (options.engineUrl) {
61410
+ const engineApiUrl = options.apiUrl || "/api/user-sdk-session";
61411
+ try {
61412
+ setLoginStatus("Conectando ao motor...");
61413
+ logoffPendingRef.current = false;
61414
+ const client2 = ensureEngine(user);
61415
+ const view = await client2.waitForView(15e3);
61416
+ const domain = engineDomain();
61417
+ const engineHasSession = (view.engine === "running" || view.engine === "starting") && !view.sessionLost;
61418
+ let token = null;
61419
+ if (engineHasSession) {
61420
+ token = readStoredToken(user);
61421
+ console.log("[Callix Dialer] motor já tem a sessão do agente; entrando sem novo login");
61422
+ } else {
61423
+ setLoginStatus("Autenticando no Callix...");
61424
+ const response = await fetch(engineApiUrl, {
61425
+ method: "POST",
61426
+ headers: { "Content-Type": "application/json" },
61427
+ body: JSON.stringify({ username: user }),
61428
+ credentials: "same-origin"
61429
+ });
61430
+ if (!response.ok) {
61431
+ const errorData = await response.json().catch(() => ({ error: "Erro desconhecido" }));
61432
+ throw new Error(errorData.error || `Falha na autenticação: ${response.status}`);
61433
+ }
61434
+ const data = await response.json();
61435
+ token = data.userSessionToken;
61436
+ client2.setSession({ domain, userSessionToken: token });
61437
+ setLoginStatus("Subindo o motor...");
61438
+ }
61439
+ await enterEngine(user, domain, token);
61440
+ console.log("[Callix Dialer] Conectado ao motor como", user);
61441
+ } catch (err) {
61442
+ console.error("[Callix Dialer] Erro ao entrar pelo motor:", err);
61443
+ setError(err instanceof Error ? err.message : "Erro ao conectar ao motor");
61444
+ setIsAuthenticated(false);
61445
+ setSessionToken(null);
61446
+ } finally {
61447
+ setIsLoading(false);
61448
+ setLoginStatus(null);
61449
+ }
61450
+ return;
61451
+ }
61452
+ if (!claimLeader(user, tabIdRef.current)) {
61453
+ console.log("[Callix Dialer] outra aba já segura a conexão deste usuário; esta aba vira membro");
61454
+ setTabRole("member");
61455
+ saveTabRole(user, "member");
61456
+ setTabDecided(true);
61457
+ setUsername(user);
61458
+ setIsLoading(false);
61459
+ setLoginStatus(null);
61460
+ return;
61461
+ }
61462
+ (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "claim" });
61463
+ cancelTakeover();
61464
+ setTabRole("leader");
61465
+ saveTabRole(user, "leader");
59935
61466
  const apiUrl = options.apiUrl || "/api/user-sdk-session";
59936
61467
  let previousToken;
59937
61468
  if (typeof window !== "undefined") {
@@ -60026,6 +61557,9 @@ function DialerWrapper({ options, containerEl }) {
60026
61557
  setError(null);
60027
61558
  setProviderKey(Date.now());
60028
61559
  console.log("[Callix Dialer] Nova providerKey gerada para forçar recriação do SDK");
61560
+ setTabNote(null);
61561
+ setTabDecided(true);
61562
+ (_b = busRef.current) == null ? void 0 : _b.post({ type: "login" });
60029
61563
  try {
60030
61564
  const configUrl = `https://${resolvedDomain}/sdk/v1/client/user-service/config`;
60031
61565
  setLoginStatus("Carregando configurações...");
@@ -60040,7 +61574,7 @@ function DialerWrapper({ options, containerEl }) {
60040
61574
  if (configResponse.ok) {
60041
61575
  const configData = await configResponse.json();
60042
61576
  console.log("[Callix Dialer] 📦 Dados da config recebidos:", JSON.stringify(configData, null, 2));
60043
- const shouldSkipQualification = ((_b = (_a2 = configData == null ? void 0 : configData.service) == null ? void 0 : _a2.outboundOptions) == null ? void 0 : _b.skipQualification) === true;
61577
+ const shouldSkipQualification = ((_d = (_c = configData == null ? void 0 : configData.service) == null ? void 0 : _c.outboundOptions) == null ? void 0 : _d.skipQualification) === true;
60044
61578
  console.log("[Callix Dialer] ✅ skipQualification configurado:", shouldSkipQualification);
60045
61579
  setSkipManualQualification(shouldSkipQualification);
60046
61580
  if (typeof window !== "undefined") {
@@ -60074,50 +61608,52 @@ function DialerWrapper({ options, containerEl }) {
60074
61608
  setError(err instanceof Error ? err.message : "Erro ao autenticar");
60075
61609
  setIsAuthenticated(false);
60076
61610
  setSessionToken(null);
61611
+ releaseLeader(user, tabIdRef.current);
61612
+ setTabRole(null);
61613
+ clearTabRole();
60077
61614
  } finally {
60078
61615
  setIsLoading(false);
60079
61616
  setLoginStatus(null);
60080
61617
  }
60081
- }, [options.apiUrl, options.domain, isAuthenticated, sessionToken]);
60082
- useEffect(() => {
60083
- if (lastPresetRef.current !== presetUsername) {
60084
- lastPresetRef.current = presetUsername;
60085
- autoLoginAttemptedRef.current = false;
60086
- }
60087
- if (!presetUsername || !autoLogin) {
60088
- return;
60089
- }
60090
- if (isAuthenticated || isLoading || isRestoringSession || error2 || autoLoginAttemptedRef.current) {
60091
- return;
60092
- }
60093
- autoLoginAttemptedRef.current = true;
60094
- void handleLogin(presetUsername);
60095
- }, [presetUsername, autoLogin, isAuthenticated, isLoading, isRestoringSession, error2, handleLogin]);
61618
+ }, [options.apiUrl, options.domain, options.engineUrl, isAuthenticated, sessionToken, ensureEngine, enterEngine, engineDomain, cancelTakeover]);
60096
61619
  useEffect(() => {
60097
- if (!autoLogin || presetUsername || !username || isAuthenticated || isLoading || isRestoringSession || error2 || autoLoginAttemptedRef.current) {
60098
- return;
60099
- }
60100
- console.log("[Callix Dialer] Auto-login com username restaurado:", username);
60101
- autoLoginAttemptedRef.current = true;
60102
- void handleLogin(username);
60103
- }, [autoLogin, presetUsername, username, isAuthenticated, isLoading, isRestoringSession, error2, handleLogin]);
60104
- const handleLogout = useCallback(async () => {
61620
+ handleLoginRef.current = handleLogin;
61621
+ }, [handleLogin]);
61622
+ const handleLogout = useCallback(async (opts) => {
61623
+ var _a2, _b;
61624
+ const broadcast = (opts == null ? void 0 : opts.broadcast) !== false;
61625
+ const keepDecision = (opts == null ? void 0 : opts.keepDecision) === true;
60105
61626
  const currentUsername = username;
60106
- if (cleanupRef.current) {
60107
- console.log("[Callix Dialer] 📴 Logout: Chamando goOffline()...");
60108
- cleanupRef.current.goOffline();
60109
- await new Promise((resolve) => setTimeout(resolve, 500));
60110
- console.log("[Callix Dialer] 🛑 Logout: Chamando stop()...");
60111
- cleanupRef.current.stop();
60112
- await new Promise((resolve) => setTimeout(resolve, 500));
61627
+ cancelTakeover();
61628
+ if (options.engineUrl) {
61629
+ if (isAuthenticatedRef.current) {
61630
+ logoffPendingRef.current = true;
61631
+ (_a2 = engineRef.current) == null ? void 0 : _a2.command({ name: "logoff" });
61632
+ }
61633
+ } else {
61634
+ if (broadcast) (_b = busRef.current) == null ? void 0 : _b.post({ type: "logout" });
61635
+ if (cleanupRef.current) {
61636
+ console.log("[Callix Dialer] 📴 Logout: Chamando goOffline()...");
61637
+ cleanupRef.current.goOffline();
61638
+ await new Promise((resolve) => setTimeout(resolve, 500));
61639
+ console.log("[Callix Dialer] 🛑 Logout: Chamando stop()...");
61640
+ cleanupRef.current.stop();
61641
+ await new Promise((resolve) => setTimeout(resolve, 500));
61642
+ }
61643
+ if (currentUsername) releaseLeader(currentUsername, tabIdRef.current);
60113
61644
  }
60114
- setUsername(presetUsername ?? null);
60115
61645
  setIsAuthenticated(false);
60116
61646
  setSessionToken(null);
60117
61647
  setCallixDomain(null);
60118
61648
  setError(null);
60119
- autoLoginAttemptedRef.current = true;
60120
- lastPresetRef.current = presetUsername ?? null;
61649
+ if (keepDecision) {
61650
+ setTabRole("member");
61651
+ if (currentUsername) saveTabRole(currentUsername, "member");
61652
+ return;
61653
+ }
61654
+ setTabRole(null);
61655
+ clearTabRole();
61656
+ setUsername(presetUsername ?? null);
60121
61657
  if (typeof window !== "undefined" && currentUsername) {
60122
61658
  try {
60123
61659
  const STORAGE_KEYS = getStorageKeys(currentUsername);
@@ -60130,14 +61666,53 @@ function DialerWrapper({ options, containerEl }) {
60130
61666
  console.warn("[Callix Dialer] Não foi possível limpar sessão:", err);
60131
61667
  }
60132
61668
  }
60133
- }, [username, presetUsername]);
60134
- const pendingAutoLogin = autoLogin && Boolean(username) && !isAuthenticated && !error2 && !autoLoginAttemptedRef.current;
61669
+ }, [username, presetUsername, options.engineUrl, cancelTakeover]);
61670
+ useEffect(() => {
61671
+ handleLogoutRef.current = handleLogout;
61672
+ }, [handleLogout]);
61673
+ const handleTakeOver = useCallback(async () => {
61674
+ const user = username;
61675
+ const bus = busRef.current;
61676
+ if (!user || !bus) return;
61677
+ setIsLoading(true);
61678
+ setError(null);
61679
+ setLoginStatus("Trazendo o discador para esta aba...");
61680
+ bus.post({ type: "takeover" });
61681
+ await new Promise((resolve) => setTimeout(resolve, 1500));
61682
+ const current = readLeader(user);
61683
+ if (current && current.tabId !== tabIdRef.current) {
61684
+ console.log("[Callix Dialer] a outra aba não soltou a conexão; continuando como membro");
61685
+ setIsLoading(false);
61686
+ setLoginStatus(null);
61687
+ return;
61688
+ }
61689
+ await handleLoginRef.current(user);
61690
+ }, [username]);
61691
+ const handleLogoutAll = useCallback(() => {
61692
+ void handleLogoutRef.current();
61693
+ }, []);
61694
+ const handleSdkSessionDropped = useCallback(
61695
+ (originator, notice) => {
61696
+ var _a2;
61697
+ const user = username;
61698
+ if (user) releaseLeader(user, tabIdRef.current);
61699
+ (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "dropped", originator });
61700
+ setTabRole(null);
61701
+ clearTabRole();
61702
+ setError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
61703
+ setIsAuthenticated(false);
61704
+ setSessionToken(null);
61705
+ setCallixDomain(null);
61706
+ },
61707
+ [username]
61708
+ );
61709
+ const deciding = Boolean(username) && !isAuthenticated && (engineMode ? engineWaiting : !tabDecided);
60135
61710
  const loginConfirmation = useMemo(() => {
60136
- if (isAuthenticated || isLoading || isRestoringSession || isDisconnecting || !username || pendingAutoLogin) {
61711
+ if (isAuthenticated || isLoading || isRestoringSession || isDisconnecting || !username || deciding) {
60137
61712
  return null;
60138
61713
  }
60139
- return { username, locked: Boolean(presetUsername) };
60140
- }, [isAuthenticated, isLoading, isRestoringSession, isDisconnecting, username, presetUsername, pendingAutoLogin]);
61714
+ return { username, locked: Boolean(presetUsername), elsewhere: !engineMode && tabRole === "member", note: tabNote };
61715
+ }, [isAuthenticated, isLoading, isRestoringSession, isDisconnecting, username, presetUsername, deciding, engineMode, tabRole, tabNote]);
60141
61716
  const handleCancelLoginConfirmation = useCallback(() => {
60142
61717
  setUsername(null);
60143
61718
  setError(null);
@@ -60169,7 +61744,7 @@ function DialerWrapper({ options, containerEl }) {
60169
61744
  }
60170
61745
  ) });
60171
61746
  }
60172
- if (!isAuthenticated || !sessionToken || !callixDomain) {
61747
+ if (!isAuthenticated || !engineMode && (!sessionToken || !callixDomain)) {
60173
61748
  return /* @__PURE__ */ jsxRuntimeExports.jsx(DialerConfigContext.Provider, { value: { skipManualQualification }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
60174
61749
  DialerComponent,
60175
61750
  {
@@ -60177,11 +61752,13 @@ function DialerWrapper({ options, containerEl }) {
60177
61752
  username,
60178
61753
  onLogin: handleLogin,
60179
61754
  onLogout: handleLogout,
60180
- isLoadingSession: isLoading || isRestoringSession || pendingAutoLogin,
61755
+ isLoadingSession: isLoading || isRestoringSession || deciding,
60181
61756
  sessionError: error2,
60182
61757
  sessionStatus: loginStatus,
60183
61758
  loginConfirmation,
60184
61759
  onCancelLoginConfirmation: handleCancelLoginConfirmation,
61760
+ onTakeOverSession: handleTakeOver,
61761
+ onLogoutAllTabs: handleLogoutAll,
60185
61762
  variant: uiVariant,
60186
61763
  portalTarget,
60187
61764
  density: options.density,
@@ -60189,12 +61766,44 @@ function DialerWrapper({ options, containerEl }) {
60189
61766
  }
60190
61767
  ) });
60191
61768
  }
61769
+ if (engineMode && engineRef.current) {
61770
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(DialerConfigContext.Provider, { value: { skipManualQualification }, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(EngineClientProvider, { client: engineRef.current, children: [
61771
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
61772
+ DialerComponent,
61773
+ {
61774
+ isAuthenticated,
61775
+ username,
61776
+ onLogin: handleLogin,
61777
+ onLogout: handleLogout,
61778
+ isLoadingSession: isLoading,
61779
+ sessionError: error2,
61780
+ sessionStatus: loginStatus,
61781
+ variant: uiVariant,
61782
+ portalTarget,
61783
+ density: options.density,
61784
+ mode: dialerMode
61785
+ }
61786
+ ),
61787
+ sessionToken && /* @__PURE__ */ jsxRuntimeExports.jsx(
61788
+ ClickToCallAgent,
61789
+ {
61790
+ enabled: clickToCall.enabled,
61791
+ pollIntervalMs: clickToCall.pollIntervalMs,
61792
+ sessionToken,
61793
+ onTask: clickToCall.onTask
61794
+ }
61795
+ )
61796
+ ] }, providerKey) });
61797
+ }
61798
+ if (!sessionToken || !callixDomain) {
61799
+ return null;
61800
+ }
60192
61801
  return /* @__PURE__ */ jsxRuntimeExports.jsx(DialerConfigContext.Provider, { value: { skipManualQualification }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
60193
61802
  clientSdkReactExports.CallixClientProvider,
60194
61803
  {
60195
61804
  domain: callixDomain,
60196
61805
  userSessionToken: sessionToken,
60197
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs(SdkCleanupManager, { ref: cleanupRef, children: [
61806
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(SdkCleanupManager, { ref: cleanupRef, onSessionDropped: handleSdkSessionDropped, children: [
60198
61807
  /* @__PURE__ */ jsxRuntimeExports.jsx(
60199
61808
  DialerComponent,
60200
61809
  {