@primoia/vocall-react 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2015 @@
1
+ // src/protocol/types.ts
2
+ var FieldType = /* @__PURE__ */ ((FieldType2) => {
3
+ FieldType2["Text"] = "text";
4
+ FieldType2["Number"] = "number";
5
+ FieldType2["Currency"] = "currency";
6
+ FieldType2["Date"] = "date";
7
+ FieldType2["Datetime"] = "datetime";
8
+ FieldType2["Email"] = "email";
9
+ FieldType2["Phone"] = "phone";
10
+ FieldType2["Masked"] = "masked";
11
+ FieldType2["Select"] = "select";
12
+ FieldType2["Autocomplete"] = "autocomplete";
13
+ FieldType2["Checkbox"] = "checkbox";
14
+ FieldType2["Radio"] = "radio";
15
+ FieldType2["Textarea"] = "textarea";
16
+ FieldType2["File"] = "file";
17
+ FieldType2["Hidden"] = "hidden";
18
+ return FieldType2;
19
+ })(FieldType || {});
20
+ var ChatRole = /* @__PURE__ */ ((ChatRole2) => {
21
+ ChatRole2["User"] = "user";
22
+ ChatRole2["Agent"] = "agent";
23
+ ChatRole2["System"] = "system";
24
+ return ChatRole2;
25
+ })(ChatRole || {});
26
+ var VocallStatus = /* @__PURE__ */ ((VocallStatus2) => {
27
+ VocallStatus2["Disconnected"] = "disconnected";
28
+ VocallStatus2["Idle"] = "idle";
29
+ VocallStatus2["Listening"] = "listening";
30
+ VocallStatus2["Recording"] = "recording";
31
+ VocallStatus2["Thinking"] = "thinking";
32
+ VocallStatus2["Speaking"] = "speaking";
33
+ VocallStatus2["Executing"] = "executing";
34
+ return VocallStatus2;
35
+ })(VocallStatus || {});
36
+
37
+ // src/voice/frame-splitter.ts
38
+ var FrameSplitter = class {
39
+ constructor(frameSizeBytes = 640) {
40
+ this.offset = 0;
41
+ this.frameSizeBytes = frameSizeBytes;
42
+ this.buffer = new Uint8Array(frameSizeBytes);
43
+ }
44
+ /**
45
+ * Feed arbitrary-length PCM bytes into the splitter.
46
+ * Whenever a full frame is assembled, `emit` is called with a copy.
47
+ */
48
+ feed(pcmBytes, emit) {
49
+ let srcPos = 0;
50
+ const srcLen = pcmBytes.length;
51
+ while (srcPos < srcLen) {
52
+ const remaining = this.frameSizeBytes - this.offset;
53
+ const available = srcLen - srcPos;
54
+ const toCopy = Math.min(available, remaining);
55
+ this.buffer.set(pcmBytes.subarray(srcPos, srcPos + toCopy), this.offset);
56
+ this.offset += toCopy;
57
+ srcPos += toCopy;
58
+ if (this.offset === this.frameSizeBytes) {
59
+ emit(new Uint8Array(this.buffer));
60
+ this.offset = 0;
61
+ }
62
+ }
63
+ }
64
+ /** Reset the internal buffer, discarding any partial frame. */
65
+ reset() {
66
+ this.offset = 0;
67
+ }
68
+ /** Number of bytes currently buffered (incomplete frame). */
69
+ get buffered() {
70
+ return this.offset;
71
+ }
72
+ };
73
+
74
+ // src/voice/web-voice-service.ts
75
+ var TARGET_SAMPLE_RATE = 16e3;
76
+ var FRAME_SIZE_BYTES = 640;
77
+ var SCRIPT_BUFFER_SIZE = 4096;
78
+ var FADE_IN_SAMPLES = 128;
79
+ var SILENT_FRAME = new Uint8Array(FRAME_SIZE_BYTES);
80
+ var WebVoiceService = class {
81
+ constructor() {
82
+ // -------------------------------------------------------------------------
83
+ // Public state
84
+ // -------------------------------------------------------------------------
85
+ this.onAudioLevel = null;
86
+ this.onPlaybackComplete = null;
87
+ this._isCapturing = false;
88
+ this._isPlaying = false;
89
+ this._isMonitoring = false;
90
+ this._isMuted = false;
91
+ // -------------------------------------------------------------------------
92
+ // Private capture state
93
+ // -------------------------------------------------------------------------
94
+ this._audioCtx = null;
95
+ this._stream = null;
96
+ this._sourceNode = null;
97
+ this._scriptNode = null;
98
+ this._splitter = new FrameSplitter(FRAME_SIZE_BYTES);
99
+ this._sendChunk = null;
100
+ // -------------------------------------------------------------------------
101
+ // Private playback state
102
+ // -------------------------------------------------------------------------
103
+ this._playbackCtx = null;
104
+ this._playbackQueue = [];
105
+ this._currentSource = null;
106
+ this._pendingDecodes = 0;
107
+ }
108
+ get isSupported() {
109
+ return typeof navigator !== "undefined" && typeof navigator.mediaDevices !== "undefined" && typeof navigator.mediaDevices.getUserMedia === "function" && typeof AudioContext !== "undefined";
110
+ }
111
+ get isCapturing() {
112
+ return this._isCapturing;
113
+ }
114
+ get isPlaying() {
115
+ return this._isPlaying;
116
+ }
117
+ get isMonitoring() {
118
+ return this._isMonitoring;
119
+ }
120
+ // -------------------------------------------------------------------------
121
+ // Capture
122
+ // -------------------------------------------------------------------------
123
+ async startCapture(sendChunk) {
124
+ if (this._isCapturing) return;
125
+ this._sendChunk = sendChunk;
126
+ await this._initCapturePipeline(false);
127
+ this._isCapturing = true;
128
+ }
129
+ stopCapture() {
130
+ this._teardownCapturePipeline();
131
+ this._isCapturing = false;
132
+ this._sendChunk = null;
133
+ }
134
+ // -------------------------------------------------------------------------
135
+ // Monitor mode (capture pipeline without sending)
136
+ // -------------------------------------------------------------------------
137
+ async startMonitor() {
138
+ if (this._isMonitoring) return;
139
+ await this._initCapturePipeline(true);
140
+ this._isMonitoring = true;
141
+ }
142
+ stopMonitor() {
143
+ this._teardownCapturePipeline();
144
+ this._isMonitoring = false;
145
+ }
146
+ // -------------------------------------------------------------------------
147
+ // Mute
148
+ // -------------------------------------------------------------------------
149
+ muteMic(muted) {
150
+ this._isMuted = muted;
151
+ if (this._stream) {
152
+ const tracks = this._stream.getAudioTracks();
153
+ for (const track of tracks) {
154
+ track.enabled = !muted;
155
+ }
156
+ }
157
+ }
158
+ // -------------------------------------------------------------------------
159
+ // Playback
160
+ // -------------------------------------------------------------------------
161
+ playAudio(wavData) {
162
+ this._playbackQueue.push(wavData);
163
+ this._pendingDecodes++;
164
+ this._isPlaying = true;
165
+ this._decodeAndEnqueue(wavData);
166
+ }
167
+ stopPlayback() {
168
+ if (this._currentSource) {
169
+ try {
170
+ this._currentSource.onended = null;
171
+ this._currentSource.stop();
172
+ } catch {
173
+ }
174
+ this._currentSource = null;
175
+ }
176
+ this._playbackQueue = [];
177
+ this._pendingDecodes = 0;
178
+ this._isPlaying = false;
179
+ }
180
+ // -------------------------------------------------------------------------
181
+ // Dispose
182
+ // -------------------------------------------------------------------------
183
+ dispose() {
184
+ this.stopCapture();
185
+ this.stopMonitor();
186
+ this.stopPlayback();
187
+ if (this._audioCtx) {
188
+ this._audioCtx.close().catch(() => {
189
+ });
190
+ this._audioCtx = null;
191
+ }
192
+ if (this._playbackCtx) {
193
+ this._playbackCtx.close().catch(() => {
194
+ });
195
+ this._playbackCtx = null;
196
+ }
197
+ this.onAudioLevel = null;
198
+ this.onPlaybackComplete = null;
199
+ }
200
+ // -------------------------------------------------------------------------
201
+ // Private: capture pipeline
202
+ // -------------------------------------------------------------------------
203
+ async _initCapturePipeline(monitorOnly) {
204
+ this._stream = await navigator.mediaDevices.getUserMedia({
205
+ audio: {
206
+ channelCount: 1,
207
+ echoCancellation: true,
208
+ noiseSuppression: false,
209
+ autoGainControl: true
210
+ }
211
+ });
212
+ if (!this._audioCtx) {
213
+ this._audioCtx = new AudioContext();
214
+ }
215
+ if (this._audioCtx.state === "suspended") {
216
+ await this._audioCtx.resume();
217
+ }
218
+ const nativeRate = this._audioCtx.sampleRate;
219
+ this._sourceNode = this._audioCtx.createMediaStreamSource(this._stream);
220
+ this._scriptNode = this._audioCtx.createScriptProcessor(SCRIPT_BUFFER_SIZE, 1, 1);
221
+ this._splitter.reset();
222
+ this._scriptNode.onaudioprocess = (event) => {
223
+ const inputData = event.inputBuffer.getChannelData(0);
224
+ this._computeAndEmitLevel(inputData);
225
+ if (this._isMuted) {
226
+ if (!monitorOnly && this._sendChunk) {
227
+ const silentPcm = this._downsampleToS16LE(new Float32Array(inputData.length), nativeRate);
228
+ this._splitter.feed(silentPcm, (frame) => {
229
+ this._sendChunk(new Uint8Array(SILENT_FRAME));
230
+ });
231
+ }
232
+ return;
233
+ }
234
+ const pcmBytes = this._downsampleToS16LE(inputData, nativeRate);
235
+ this._splitter.feed(pcmBytes, (frame) => {
236
+ if (!monitorOnly && this._sendChunk) {
237
+ this._sendChunk(frame);
238
+ }
239
+ });
240
+ };
241
+ this._sourceNode.connect(this._scriptNode);
242
+ this._scriptNode.connect(this._audioCtx.destination);
243
+ }
244
+ _teardownCapturePipeline() {
245
+ if (this._scriptNode) {
246
+ this._scriptNode.onaudioprocess = null;
247
+ this._scriptNode.disconnect();
248
+ this._scriptNode = null;
249
+ }
250
+ if (this._sourceNode) {
251
+ this._sourceNode.disconnect();
252
+ this._sourceNode = null;
253
+ }
254
+ if (this._stream) {
255
+ for (const track of this._stream.getTracks()) {
256
+ track.stop();
257
+ }
258
+ this._stream = null;
259
+ }
260
+ this._splitter.reset();
261
+ this._isMuted = false;
262
+ }
263
+ // -------------------------------------------------------------------------
264
+ // Private: audio processing
265
+ // -------------------------------------------------------------------------
266
+ /**
267
+ * Downsample Float32 audio at native sample rate to S16LE at TARGET_SAMPLE_RATE
268
+ * using linear interpolation.
269
+ */
270
+ _downsampleToS16LE(input, fromRate) {
271
+ const ratio = fromRate / TARGET_SAMPLE_RATE;
272
+ const outputLen = Math.floor(input.length / ratio);
273
+ const output = new Uint8Array(outputLen * 2);
274
+ const view = new DataView(output.buffer);
275
+ for (let i = 0; i < outputLen; i++) {
276
+ const srcIdx = i * ratio;
277
+ const srcFloor = Math.floor(srcIdx);
278
+ const srcCeil = Math.min(srcFloor + 1, input.length - 1);
279
+ const frac = srcIdx - srcFloor;
280
+ let sample = input[srcFloor] + (input[srcCeil] - input[srcFloor]) * frac;
281
+ if (sample > 1) sample = 1;
282
+ else if (sample < -1) sample = -1;
283
+ const s16 = Math.round(sample * 32767);
284
+ view.setInt16(i * 2, s16, true);
285
+ }
286
+ return output;
287
+ }
288
+ /**
289
+ * Compute RMS level from Float32 audio samples.
290
+ * Result is scaled by 4 and clamped to [0, 1].
291
+ */
292
+ _computeAndEmitLevel(samples) {
293
+ if (!this.onAudioLevel) return;
294
+ let sum = 0;
295
+ for (let i = 0; i < samples.length; i++) {
296
+ sum += samples[i] * samples[i];
297
+ }
298
+ const rms = Math.sqrt(sum / samples.length);
299
+ const level = Math.min(rms * 4, 1);
300
+ this.onAudioLevel(level);
301
+ }
302
+ // -------------------------------------------------------------------------
303
+ // Private: playback pipeline
304
+ // -------------------------------------------------------------------------
305
+ async _decodeAndEnqueue(wavData) {
306
+ if (!this._playbackCtx) {
307
+ this._playbackCtx = new AudioContext();
308
+ }
309
+ if (this._playbackCtx.state === "suspended") {
310
+ await this._playbackCtx.resume();
311
+ }
312
+ try {
313
+ const arrayBuffer = wavData.buffer.slice(
314
+ wavData.byteOffset,
315
+ wavData.byteOffset + wavData.byteLength
316
+ );
317
+ const audioBuffer = await this._playbackCtx.decodeAudioData(arrayBuffer);
318
+ this._applyFadeIn(audioBuffer);
319
+ this._pendingDecodes--;
320
+ if (!this._isPlaying) return;
321
+ if (!this._currentSource) {
322
+ this._playBuffer(audioBuffer);
323
+ }
324
+ } catch {
325
+ this._pendingDecodes--;
326
+ this._checkPlaybackComplete();
327
+ }
328
+ }
329
+ _applyFadeIn(audioBuffer) {
330
+ const fadeLen = Math.min(FADE_IN_SAMPLES, audioBuffer.length);
331
+ for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
332
+ const data = audioBuffer.getChannelData(ch);
333
+ for (let i = 0; i < fadeLen; i++) {
334
+ data[i] *= i / fadeLen;
335
+ }
336
+ }
337
+ }
338
+ _playBuffer(audioBuffer) {
339
+ if (!this._playbackCtx) return;
340
+ const source = this._playbackCtx.createBufferSource();
341
+ source.buffer = audioBuffer;
342
+ source.connect(this._playbackCtx.destination);
343
+ source.onended = () => {
344
+ this._currentSource = null;
345
+ this._playNextChunk();
346
+ };
347
+ this._currentSource = source;
348
+ source.start();
349
+ }
350
+ _playNextChunk() {
351
+ if (this._playbackQueue.length > 0) {
352
+ this._playbackQueue.shift();
353
+ }
354
+ if (this._playbackQueue.length > 0) {
355
+ const nextData = this._playbackQueue[0];
356
+ this._pendingDecodes++;
357
+ this._decodeAndEnqueue(nextData);
358
+ } else {
359
+ this._checkPlaybackComplete();
360
+ }
361
+ }
362
+ _checkPlaybackComplete() {
363
+ if (this._playbackQueue.length === 0 && this._pendingDecodes <= 0) {
364
+ this._isPlaying = false;
365
+ this.onPlaybackComplete?.();
366
+ }
367
+ }
368
+ };
369
+
370
+ // src/client/vocall-client.ts
371
+ var TARGET_SAMPLE_RATE2 = 16e3;
372
+ var _VocallClient = class _VocallClient {
373
+ // -----------------------------------------------------------------------
374
+ // Constructor
375
+ // -----------------------------------------------------------------------
376
+ constructor(serverUrl, options) {
377
+ // -----------------------------------------------------------------------
378
+ // Observable state (getters + onChange)
379
+ // -----------------------------------------------------------------------
380
+ this._status = "disconnected" /* Disconnected */;
381
+ this._connected = false;
382
+ this._messages = [];
383
+ this._sessionId = null;
384
+ // -----------------------------------------------------------------------
385
+ // Voice state (public getters)
386
+ // -----------------------------------------------------------------------
387
+ this._voiceEnabled = false;
388
+ this._voiceState = "idle";
389
+ this._recording = false;
390
+ this._partialTranscription = null;
391
+ this._audioLevel = 0;
392
+ // -----------------------------------------------------------------------
393
+ // Voice internals
394
+ // -----------------------------------------------------------------------
395
+ this._voiceWs = null;
396
+ this._alwaysListening = false;
397
+ this._ttsActive = false;
398
+ this._ttsEndReceived = false;
399
+ this._llmDone = false;
400
+ this._pendingTtsChunks = 0;
401
+ this._voice = null;
402
+ // -----------------------------------------------------------------------
403
+ // Change notification (React hooks subscribe here)
404
+ // -----------------------------------------------------------------------
405
+ this._listeners = /* @__PURE__ */ new Set();
406
+ // -----------------------------------------------------------------------
407
+ // Callbacks (set by the host app)
408
+ // -----------------------------------------------------------------------
409
+ this.onNavigate = null;
410
+ this.onToast = null;
411
+ this.onConfirm = null;
412
+ this.onOpenModal = null;
413
+ this.onCloseModal = null;
414
+ // -----------------------------------------------------------------------
415
+ // Field & action registries
416
+ // -----------------------------------------------------------------------
417
+ this._fields = /* @__PURE__ */ new Map();
418
+ this._actions = /* @__PURE__ */ new Map();
419
+ // -----------------------------------------------------------------------
420
+ // Internal state
421
+ // -----------------------------------------------------------------------
422
+ this._ws = null;
423
+ this._manifest = null;
424
+ this._tokenBuffer = "";
425
+ this._streamingMessage = null;
426
+ this._reconnectTimer = null;
427
+ this._reconnectAttempts = 0;
428
+ this._intentionalDisconnect = false;
429
+ this._pendingConfirmSeq = -1;
430
+ this.serverUrl = serverUrl;
431
+ this.token = options?.token;
432
+ this._visitorId = options?.visitorId ?? _VocallClient._loadOrCreateVisitorId();
433
+ }
434
+ get visitorId() {
435
+ return this._visitorId;
436
+ }
437
+ set visitorId(id) {
438
+ this._visitorId = id;
439
+ }
440
+ get status() {
441
+ return this._status;
442
+ }
443
+ get connected() {
444
+ return this._connected;
445
+ }
446
+ get messages() {
447
+ return this._messages;
448
+ }
449
+ get sessionId() {
450
+ return this._sessionId;
451
+ }
452
+ get voiceEnabled() {
453
+ return this._voiceEnabled;
454
+ }
455
+ /** Whether the platform supports voice capture (Web Audio API). */
456
+ get voiceSupported() {
457
+ return this._getVoice().isSupported;
458
+ }
459
+ get voiceState() {
460
+ return this._voiceState;
461
+ }
462
+ get recording() {
463
+ return this._recording;
464
+ }
465
+ get partialTranscription() {
466
+ return this._partialTranscription;
467
+ }
468
+ get audioLevel() {
469
+ return this._audioLevel;
470
+ }
471
+ _getVoice() {
472
+ if (!this._voice) {
473
+ this._voice = new WebVoiceService();
474
+ this._voice.onAudioLevel = (level) => {
475
+ this._audioLevel = level;
476
+ this._notify();
477
+ };
478
+ this._voice.onPlaybackComplete = () => {
479
+ this._onPlaybackComplete();
480
+ };
481
+ }
482
+ return this._voice;
483
+ }
484
+ subscribe(listener) {
485
+ this._listeners.add(listener);
486
+ return () => {
487
+ this._listeners.delete(listener);
488
+ };
489
+ }
490
+ _notify() {
491
+ for (const listener of this._listeners) {
492
+ listener();
493
+ }
494
+ }
495
+ // -----------------------------------------------------------------------
496
+ // Visitor ID persistence
497
+ // -----------------------------------------------------------------------
498
+ static _generateUuid() {
499
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
500
+ return crypto.randomUUID();
501
+ }
502
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
503
+ const r = Math.random() * 16 | 0;
504
+ const v = c === "x" ? r : r & 3 | 8;
505
+ return v.toString(16);
506
+ });
507
+ }
508
+ static _loadOrCreateVisitorId() {
509
+ const STORAGE_KEY = "vocall_visitor_id";
510
+ try {
511
+ const existing = localStorage.getItem(STORAGE_KEY);
512
+ if (existing) return existing;
513
+ const id = _VocallClient._generateUuid();
514
+ localStorage.setItem(STORAGE_KEY, id);
515
+ return id;
516
+ } catch {
517
+ return _VocallClient._generateUuid();
518
+ }
519
+ }
520
+ // -----------------------------------------------------------------------
521
+ // Field & action registration
522
+ // -----------------------------------------------------------------------
523
+ registerField(screenId, fieldId, entry) {
524
+ if (!this._fields.has(screenId)) {
525
+ this._fields.set(screenId, /* @__PURE__ */ new Map());
526
+ }
527
+ this._fields.get(screenId).set(fieldId, entry);
528
+ }
529
+ unregisterField(screenId, fieldId) {
530
+ this._fields.get(screenId)?.delete(fieldId);
531
+ if (this._fields.get(screenId)?.size === 0) {
532
+ this._fields.delete(screenId);
533
+ }
534
+ }
535
+ unregisterScreen(screenId) {
536
+ this._fields.delete(screenId);
537
+ this._actions.delete(screenId);
538
+ }
539
+ registerAction(screenId, actionId, callback) {
540
+ if (!this._actions.has(screenId)) {
541
+ this._actions.set(screenId, /* @__PURE__ */ new Map());
542
+ }
543
+ this._actions.get(screenId).set(actionId, callback);
544
+ }
545
+ unregisterAction(screenId, actionId) {
546
+ this._actions.get(screenId)?.delete(actionId);
547
+ }
548
+ findField(fieldId) {
549
+ for (const screen of this._fields.values()) {
550
+ if (screen.has(fieldId)) return screen.get(fieldId);
551
+ }
552
+ return void 0;
553
+ }
554
+ findAction(actionId) {
555
+ for (const screen of this._actions.values()) {
556
+ if (screen.has(actionId)) return screen.get(actionId);
557
+ }
558
+ return void 0;
559
+ }
560
+ // -----------------------------------------------------------------------
561
+ // Connection
562
+ // -----------------------------------------------------------------------
563
+ connect(manifest) {
564
+ this._manifest = manifest;
565
+ this._intentionalDisconnect = false;
566
+ this._doConnect();
567
+ }
568
+ _doConnect() {
569
+ this.disconnect(false);
570
+ const url = new URL(this.serverUrl);
571
+ url.searchParams.set("visitor_id", this._visitorId);
572
+ if (this.token) {
573
+ url.searchParams.set("token", this.token);
574
+ }
575
+ try {
576
+ this._ws = new WebSocket(url.toString());
577
+ this._ws.onmessage = (event) => {
578
+ this._onMessage(event.data);
579
+ };
580
+ this._ws.onclose = () => {
581
+ this._onDone();
582
+ };
583
+ this._ws.onerror = () => {
584
+ this._onError();
585
+ };
586
+ } catch {
587
+ this._scheduleReconnect();
588
+ }
589
+ }
590
+ disconnect(intentional = true) {
591
+ if (intentional) this._intentionalDisconnect = true;
592
+ if (this._reconnectTimer) {
593
+ clearTimeout(this._reconnectTimer);
594
+ this._reconnectTimer = null;
595
+ }
596
+ this._closeVoiceWs();
597
+ if (this._voice) {
598
+ this._voice.stopCapture();
599
+ this._voice.stopPlayback();
600
+ }
601
+ this._voiceState = "idle";
602
+ this._recording = false;
603
+ this._partialTranscription = null;
604
+ this._audioLevel = 0;
605
+ this._alwaysListening = false;
606
+ this._resetTtsState();
607
+ if (this._ws) {
608
+ this._ws.onclose = null;
609
+ this._ws.onerror = null;
610
+ this._ws.onmessage = null;
611
+ this._ws.close();
612
+ this._ws = null;
613
+ }
614
+ this._sessionId = null;
615
+ this._tokenBuffer = "";
616
+ this._streamingMessage = null;
617
+ this._pendingConfirmSeq = -1;
618
+ this._status = "disconnected" /* Disconnected */;
619
+ this._connected = false;
620
+ this._notify();
621
+ }
622
+ // -----------------------------------------------------------------------
623
+ // Sending messages
624
+ // -----------------------------------------------------------------------
625
+ sendText(text) {
626
+ if (!text.trim() || !this._ws) return;
627
+ this._addMessage({ role: "user" /* User */, text, timestamp: /* @__PURE__ */ new Date() });
628
+ this._wsSend({ type: "text", message: text });
629
+ }
630
+ sendConfirm(seq, confirmed) {
631
+ this._wsSend({ type: "confirm", seq, confirmed });
632
+ this._pendingConfirmSeq = -1;
633
+ }
634
+ sendResult(seq, results, state) {
635
+ const msg = { type: "result", seq, results };
636
+ if (state) msg.state = state;
637
+ this._wsSend(msg);
638
+ }
639
+ sendState(state) {
640
+ this._wsSend(state);
641
+ }
642
+ _wsSend(obj) {
643
+ if (this._ws && this._ws.readyState === WebSocket.OPEN) {
644
+ this._ws.send(JSON.stringify(obj));
645
+ }
646
+ }
647
+ // -----------------------------------------------------------------------
648
+ // Incoming message handling
649
+ // -----------------------------------------------------------------------
650
+ _onMessage(raw) {
651
+ let json;
652
+ try {
653
+ json = JSON.parse(raw);
654
+ } catch {
655
+ return;
656
+ }
657
+ const type = json["type"];
658
+ if (!type) return;
659
+ switch (type) {
660
+ case "config":
661
+ this._handleConfig(json);
662
+ break;
663
+ case "chat":
664
+ this._handleChat(json);
665
+ break;
666
+ case "chat_token":
667
+ this._handleChatToken(json);
668
+ break;
669
+ case "chat_end":
670
+ this._flushTokenBuffer();
671
+ break;
672
+ case "status":
673
+ this._handleStatus(json);
674
+ break;
675
+ case "command":
676
+ this._handleCommand(json);
677
+ break;
678
+ case "error":
679
+ this._handleError(json);
680
+ break;
681
+ case "history":
682
+ this._handleHistory(json);
683
+ break;
684
+ }
685
+ }
686
+ _handleConfig(config) {
687
+ this._sessionId = config.sessionId;
688
+ this._status = "idle" /* Idle */;
689
+ this._connected = true;
690
+ this._reconnectAttempts = 0;
691
+ this._voiceEnabled = config.features?.voice === true;
692
+ this._notify();
693
+ if (this._manifest) {
694
+ this._wsSend(this._manifest);
695
+ }
696
+ }
697
+ _handleHistory(json) {
698
+ const items = json["messages"];
699
+ if (!items || !items.length) return;
700
+ this._messages = [];
701
+ for (const item of items) {
702
+ const role = item["role"];
703
+ const content = item["content"];
704
+ if (!content) continue;
705
+ const chatRole = role === "user" ? "user" /* User */ : role === "assistant" ? "agent" /* Agent */ : null;
706
+ if (!chatRole) continue;
707
+ this._messages.push({ role: chatRole, text: content, timestamp: /* @__PURE__ */ new Date() });
708
+ }
709
+ this._notify();
710
+ }
711
+ _handleChat(chat) {
712
+ this._flushTokenBuffer();
713
+ this._addMessage({
714
+ role: "agent" /* Agent */,
715
+ text: chat.message,
716
+ timestamp: /* @__PURE__ */ new Date()
717
+ });
718
+ }
719
+ _handleChatToken(token) {
720
+ this._tokenBuffer += token.token;
721
+ if (this._streamingMessage) {
722
+ this._streamingMessage.text = this._tokenBuffer;
723
+ } else {
724
+ this._streamingMessage = {
725
+ role: "agent" /* Agent */,
726
+ text: this._tokenBuffer,
727
+ timestamp: /* @__PURE__ */ new Date()
728
+ };
729
+ this._messages.push(this._streamingMessage);
730
+ }
731
+ this._notify();
732
+ }
733
+ _flushTokenBuffer() {
734
+ this._tokenBuffer = "";
735
+ this._streamingMessage = null;
736
+ }
737
+ _handleStatus(status) {
738
+ this._status = this._mapStatus(status.status);
739
+ this._notify();
740
+ }
741
+ _mapStatus(status) {
742
+ switch (status) {
743
+ case "idle":
744
+ return "idle" /* Idle */;
745
+ case "listening":
746
+ return "listening" /* Listening */;
747
+ case "recording":
748
+ return "recording" /* Recording */;
749
+ case "thinking":
750
+ return "thinking" /* Thinking */;
751
+ case "speaking":
752
+ return "speaking" /* Speaking */;
753
+ case "executing":
754
+ return "executing" /* Executing */;
755
+ default:
756
+ return "idle" /* Idle */;
757
+ }
758
+ }
759
+ _handleError(error) {
760
+ this._addMessage({
761
+ role: "system" /* System */,
762
+ text: error.message,
763
+ timestamp: /* @__PURE__ */ new Date()
764
+ });
765
+ this._status = "idle" /* Idle */;
766
+ this._notify();
767
+ }
768
+ // -----------------------------------------------------------------------
769
+ // Command execution
770
+ // -----------------------------------------------------------------------
771
+ async _handleCommand(cmd) {
772
+ const results = new Array(cmd.actions.length);
773
+ const sequentialIndices = [];
774
+ const parallelIndices = [];
775
+ for (let i = 0; i < cmd.actions.length; i++) {
776
+ if (_VocallClient.SEQUENTIAL_ACTIONS.has(cmd.actions[i].do)) {
777
+ sequentialIndices.push(i);
778
+ } else {
779
+ parallelIndices.push(i);
780
+ }
781
+ }
782
+ for (const i of sequentialIndices) {
783
+ try {
784
+ await this._executeAction(cmd.actions[i], cmd.seq);
785
+ results[i] = { index: i, success: true };
786
+ } catch (e) {
787
+ results[i] = { index: i, success: false, error: String(e) };
788
+ }
789
+ }
790
+ if (parallelIndices.length > 0) {
791
+ await Promise.all(
792
+ parallelIndices.map(async (i) => {
793
+ try {
794
+ await this._executeAction(cmd.actions[i], cmd.seq);
795
+ results[i] = { index: i, success: true };
796
+ } catch (e) {
797
+ results[i] = { index: i, success: false, error: String(e) };
798
+ }
799
+ })
800
+ );
801
+ }
802
+ for (let i = 0; i < results.length; i++) {
803
+ if (!results[i]) {
804
+ results[i] = { index: i, success: false, error: "not executed" };
805
+ }
806
+ }
807
+ this.sendResult(cmd.seq, results);
808
+ }
809
+ async _executeAction(action, seq) {
810
+ switch (action.do) {
811
+ case "navigate":
812
+ await this._execNavigate(action);
813
+ break;
814
+ case "fill":
815
+ await this._execFill(action);
816
+ break;
817
+ case "clear":
818
+ this._execClear(action);
819
+ break;
820
+ case "select":
821
+ this._execSelect(action);
822
+ break;
823
+ case "click":
824
+ await this._execClick(action);
825
+ break;
826
+ case "focus":
827
+ this._execFocus(action);
828
+ break;
829
+ case "highlight":
830
+ this._execHighlight(action);
831
+ break;
832
+ case "scroll_to":
833
+ this._execScrollTo(action);
834
+ break;
835
+ case "show_toast":
836
+ this._execShowToast(action);
837
+ break;
838
+ case "ask_confirm":
839
+ this._execAskConfirm(action, seq);
840
+ break;
841
+ case "open_modal":
842
+ this._execOpenModal(action);
843
+ break;
844
+ case "close_modal":
845
+ this._execCloseModal();
846
+ break;
847
+ case "enable":
848
+ this._execEnable(action);
849
+ break;
850
+ case "disable":
851
+ this._execDisable(action);
852
+ break;
853
+ }
854
+ }
855
+ async _execNavigate(action) {
856
+ const screenId = action.screen;
857
+ if (!screenId) return;
858
+ this.onNavigate?.(screenId);
859
+ await this._sleep(_VocallClient.RETRY_DELAY_MS);
860
+ }
861
+ async _execFill(action) {
862
+ const fieldId = action.field;
863
+ if (!fieldId) return;
864
+ let entry = this.findField(fieldId);
865
+ for (let i = 0; i < _VocallClient.MAX_RETRIES && !entry; i++) {
866
+ await this._sleep(_VocallClient.RETRY_DELAY_MS);
867
+ entry = this.findField(fieldId);
868
+ }
869
+ if (!entry) {
870
+ throw new Error(`field "${fieldId}" not registered`);
871
+ }
872
+ const value = action.value != null ? String(action.value) : "";
873
+ const animate = action.animate ?? "typewriter";
874
+ if (animate === "typewriter" && value.length > 1) {
875
+ await this._typewriterFill(entry, value, action.speed ?? 40);
876
+ } else {
877
+ entry.setValue(value);
878
+ }
879
+ }
880
+ async _typewriterFill(entry, value, speedMs) {
881
+ entry.element.classList.add("filling");
882
+ entry.element.focus();
883
+ entry.setValue("");
884
+ for (let i = 0; i < value.length; i++) {
885
+ entry.setValue(value.substring(0, i + 1));
886
+ await this._sleep(speedMs);
887
+ }
888
+ entry.element.classList.remove("filling");
889
+ }
890
+ _execClear(action) {
891
+ const fieldId = action.field;
892
+ if (!fieldId) return;
893
+ const entry = this.findField(fieldId);
894
+ if (entry) entry.setValue("");
895
+ }
896
+ _execSelect(action) {
897
+ const fieldId = action.field;
898
+ if (!fieldId) return;
899
+ const entry = this.findField(fieldId);
900
+ if (entry) entry.setValue(String(action.value ?? ""));
901
+ }
902
+ async _execClick(action) {
903
+ const actionId = action.action;
904
+ if (!actionId) return;
905
+ let callback = this.findAction(actionId);
906
+ for (let i = 0; i < _VocallClient.MAX_RETRIES && !callback; i++) {
907
+ await this._sleep(_VocallClient.RETRY_DELAY_MS);
908
+ callback = this.findAction(actionId);
909
+ }
910
+ if (!callback) {
911
+ throw new Error(`action "${actionId}" not registered`);
912
+ }
913
+ await callback();
914
+ }
915
+ _execFocus(action) {
916
+ const fieldId = action.field;
917
+ if (!fieldId) return;
918
+ const entry = this.findField(fieldId);
919
+ if (entry) entry.element.focus();
920
+ }
921
+ _execHighlight(action) {
922
+ const fieldId = action.field;
923
+ if (!fieldId) return;
924
+ const entry = this.findField(fieldId);
925
+ if (!entry) return;
926
+ entry.element.scrollIntoView({ behavior: "smooth", block: "center" });
927
+ entry.element.classList.add("highlighted");
928
+ const duration = action.duration ?? 2e3;
929
+ setTimeout(() => entry.element.classList.remove("highlighted"), duration);
930
+ }
931
+ _execScrollTo(action) {
932
+ const fieldId = action.field;
933
+ if (!fieldId) return;
934
+ const entry = this.findField(fieldId);
935
+ if (entry) entry.element.scrollIntoView({ behavior: "smooth", block: "center" });
936
+ }
937
+ _execShowToast(action) {
938
+ const message = action.message ?? "";
939
+ const level = action.level ?? "info";
940
+ const duration = action.duration;
941
+ this.onToast?.(message, level, duration);
942
+ }
943
+ _execAskConfirm(action, seq) {
944
+ this._pendingConfirmSeq = seq;
945
+ const message = action.message ?? "Confirmar?";
946
+ this.onConfirm?.(seq, message);
947
+ }
948
+ _execOpenModal(action) {
949
+ const modalId = action.modal;
950
+ if (!modalId) return;
951
+ this.onOpenModal?.(modalId, action.query);
952
+ }
953
+ _execCloseModal() {
954
+ this.onCloseModal?.();
955
+ }
956
+ _execEnable(action) {
957
+ const entry = action.field ? this.findField(action.field) : void 0;
958
+ if (entry && entry.element instanceof HTMLInputElement) {
959
+ entry.element.disabled = false;
960
+ }
961
+ }
962
+ _execDisable(action) {
963
+ const entry = action.field ? this.findField(action.field) : void 0;
964
+ if (entry && entry.element instanceof HTMLInputElement) {
965
+ entry.element.disabled = true;
966
+ }
967
+ }
968
+ // -----------------------------------------------------------------------
969
+ // Voice: public methods
970
+ // -----------------------------------------------------------------------
971
+ async startAlwaysListening() {
972
+ if (!this._getVoice().isSupported || !this._connected) return;
973
+ this._alwaysListening = true;
974
+ await this._openVoiceWs(false);
975
+ const voice = this._getVoice();
976
+ await voice.startCapture((chunk) => {
977
+ if (this._voiceWs && this._voiceWs.readyState === WebSocket.OPEN) {
978
+ this._voiceWs.send(chunk);
979
+ }
980
+ });
981
+ this._voiceState = "listening";
982
+ this._recording = false;
983
+ this._notify();
984
+ }
985
+ stopAlwaysListening() {
986
+ this._alwaysListening = false;
987
+ const voice = this._getVoice();
988
+ voice.stopCapture();
989
+ voice.stopPlayback();
990
+ this._closeVoiceWs();
991
+ this._voiceState = "idle";
992
+ this._recording = false;
993
+ this._partialTranscription = null;
994
+ this._audioLevel = 0;
995
+ this._resetTtsState();
996
+ this._notify();
997
+ }
998
+ async startRecording() {
999
+ if (!this._getVoice().isSupported || !this._connected) return;
1000
+ this._alwaysListening = false;
1001
+ await this._openVoiceWs(true);
1002
+ const voice = this._getVoice();
1003
+ await voice.startCapture((chunk) => {
1004
+ if (this._voiceWs && this._voiceWs.readyState === WebSocket.OPEN) {
1005
+ this._voiceWs.send(chunk);
1006
+ }
1007
+ });
1008
+ this._voiceState = "recording";
1009
+ this._recording = true;
1010
+ this._notify();
1011
+ }
1012
+ stopRecording() {
1013
+ const voice = this._getVoice();
1014
+ voice.stopCapture();
1015
+ this._recording = false;
1016
+ this._voiceState = "thinking";
1017
+ this._notify();
1018
+ if (this._voiceWs && this._voiceWs.readyState === WebSocket.OPEN) {
1019
+ this._voiceWs.send(JSON.stringify({ type: "eof" }));
1020
+ }
1021
+ }
1022
+ interrupt() {
1023
+ const voice = this._getVoice();
1024
+ voice.stopPlayback();
1025
+ this._ttsActive = false;
1026
+ if (this._voiceWs && this._voiceWs.readyState === WebSocket.OPEN) {
1027
+ this._voiceWs.send(JSON.stringify({ type: "interrupt" }));
1028
+ this._voiceWs.send(JSON.stringify({ type: "tts_state", active: false }));
1029
+ }
1030
+ if (this._alwaysListening) {
1031
+ voice.muteMic(false);
1032
+ this._voiceState = "listening";
1033
+ } else {
1034
+ this._voiceState = "idle";
1035
+ }
1036
+ this._partialTranscription = null;
1037
+ this._resetTtsState();
1038
+ this._notify();
1039
+ }
1040
+ // -----------------------------------------------------------------------
1041
+ // Voice: private WS management
1042
+ // -----------------------------------------------------------------------
1043
+ async _openVoiceWs(directMode) {
1044
+ this._closeVoiceWs();
1045
+ const url = new URL(this.serverUrl);
1046
+ url.pathname = "/ws/stream";
1047
+ url.searchParams.set("visitor_id", this._visitorId);
1048
+ if (this.token) {
1049
+ url.searchParams.set("token", this.token);
1050
+ }
1051
+ return new Promise((resolve, reject) => {
1052
+ try {
1053
+ this._voiceWs = new WebSocket(url.toString());
1054
+ this._voiceWs.binaryType = "arraybuffer";
1055
+ this._voiceWs.onopen = () => {
1056
+ const config = {
1057
+ type: "config",
1058
+ sample_rate: TARGET_SAMPLE_RATE2
1059
+ };
1060
+ if (directMode) {
1061
+ config["mode"] = "direct";
1062
+ }
1063
+ this._voiceWs.send(JSON.stringify(config));
1064
+ resolve();
1065
+ };
1066
+ this._voiceWs.onmessage = (event) => {
1067
+ this._onVoiceStreamMessage(event);
1068
+ };
1069
+ this._voiceWs.onclose = () => {
1070
+ this._voiceWs = null;
1071
+ };
1072
+ this._voiceWs.onerror = () => {
1073
+ this._voiceWs = null;
1074
+ reject(new Error("Voice WebSocket connection failed"));
1075
+ };
1076
+ } catch (e) {
1077
+ reject(e);
1078
+ }
1079
+ });
1080
+ }
1081
+ _closeVoiceWs() {
1082
+ if (this._voiceWs) {
1083
+ this._voiceWs.onclose = null;
1084
+ this._voiceWs.onerror = null;
1085
+ this._voiceWs.onmessage = null;
1086
+ this._voiceWs.close();
1087
+ this._voiceWs = null;
1088
+ }
1089
+ }
1090
+ _resetTtsState() {
1091
+ this._ttsActive = false;
1092
+ this._ttsEndReceived = false;
1093
+ this._llmDone = false;
1094
+ this._pendingTtsChunks = 0;
1095
+ }
1096
+ // -----------------------------------------------------------------------
1097
+ // Voice: stream message handler
1098
+ // -----------------------------------------------------------------------
1099
+ _onVoiceStreamMessage(event) {
1100
+ const { data } = event;
1101
+ if (data instanceof ArrayBuffer) {
1102
+ const voice = this._getVoice();
1103
+ voice.playAudio(new Uint8Array(data));
1104
+ return;
1105
+ }
1106
+ let json;
1107
+ try {
1108
+ json = JSON.parse(data);
1109
+ } catch {
1110
+ return;
1111
+ }
1112
+ const type = json["type"];
1113
+ if (!type) return;
1114
+ switch (type) {
1115
+ case "wake_word":
1116
+ this._voiceState = "recording";
1117
+ this._recording = true;
1118
+ this._partialTranscription = null;
1119
+ this._notify();
1120
+ break;
1121
+ case "partial":
1122
+ this._partialTranscription = json["text"] ?? null;
1123
+ this._notify();
1124
+ break;
1125
+ case "transcription":
1126
+ this._partialTranscription = null;
1127
+ this._voiceState = "thinking";
1128
+ this._recording = false;
1129
+ this._notify();
1130
+ break;
1131
+ case "llm_start":
1132
+ this._llmDone = false;
1133
+ break;
1134
+ case "llm_token": {
1135
+ const text = json["text"];
1136
+ if (text) {
1137
+ this._tokenBuffer += text;
1138
+ if (this._streamingMessage) {
1139
+ this._streamingMessage.text = this._tokenBuffer;
1140
+ } else {
1141
+ this._streamingMessage = {
1142
+ role: "agent" /* Agent */,
1143
+ text: this._tokenBuffer,
1144
+ timestamp: /* @__PURE__ */ new Date()
1145
+ };
1146
+ this._messages = [...this._messages, this._streamingMessage];
1147
+ }
1148
+ this._notify();
1149
+ }
1150
+ break;
1151
+ }
1152
+ case "llm_end":
1153
+ this._llmDone = true;
1154
+ this._flushTokenBuffer();
1155
+ this._ttsEndReceived = this._pendingTtsChunks <= 0 && this._llmDone;
1156
+ break;
1157
+ case "tts_start":
1158
+ this._pendingTtsChunks++;
1159
+ this._ttsActive = true;
1160
+ this._ttsEndReceived = false;
1161
+ this._voiceState = "speaking";
1162
+ this._getVoice().muteMic(true);
1163
+ this._notify();
1164
+ break;
1165
+ case "tts_end":
1166
+ this._pendingTtsChunks--;
1167
+ if (this._pendingTtsChunks <= 0 && this._llmDone) {
1168
+ this._ttsEndReceived = true;
1169
+ }
1170
+ break;
1171
+ case "interrupted":
1172
+ this._getVoice().stopPlayback();
1173
+ this._ttsActive = false;
1174
+ this._resetTtsState();
1175
+ if (this._alwaysListening) {
1176
+ this._getVoice().muteMic(false);
1177
+ this._voiceState = "listening";
1178
+ } else {
1179
+ this._voiceState = "idle";
1180
+ }
1181
+ this._partialTranscription = null;
1182
+ this._notify();
1183
+ break;
1184
+ case "conversation_end":
1185
+ this._getVoice().stopPlayback();
1186
+ this._ttsActive = false;
1187
+ this._resetTtsState();
1188
+ if (this._alwaysListening) {
1189
+ this._getVoice().muteMic(false);
1190
+ this._voiceState = "listening";
1191
+ } else {
1192
+ this._voiceState = "idle";
1193
+ this._closeVoiceWs();
1194
+ this._getVoice().stopCapture();
1195
+ this._recording = false;
1196
+ }
1197
+ this._partialTranscription = null;
1198
+ this._notify();
1199
+ break;
1200
+ }
1201
+ }
1202
+ // -----------------------------------------------------------------------
1203
+ // Voice: playback completion handler
1204
+ // -----------------------------------------------------------------------
1205
+ _onPlaybackComplete() {
1206
+ if (this._alwaysListening) {
1207
+ if (!this._ttsEndReceived) return;
1208
+ this._finalizeTts();
1209
+ } else {
1210
+ }
1211
+ }
1212
+ _finalizeTts() {
1213
+ this._ttsActive = false;
1214
+ if (this._voiceWs && this._voiceWs.readyState === WebSocket.OPEN) {
1215
+ this._voiceWs.send(JSON.stringify({ type: "tts_state", active: false }));
1216
+ }
1217
+ this._getVoice().muteMic(false);
1218
+ this._voiceState = "listening";
1219
+ this._recording = false;
1220
+ this._partialTranscription = null;
1221
+ this._resetTtsState();
1222
+ this._notify();
1223
+ }
1224
+ // -----------------------------------------------------------------------
1225
+ // Message management
1226
+ // -----------------------------------------------------------------------
1227
+ _addMessage(msg) {
1228
+ this._messages = [...this._messages, msg];
1229
+ this._notify();
1230
+ }
1231
+ clearMessages() {
1232
+ this._messages = [];
1233
+ this._tokenBuffer = "";
1234
+ this._streamingMessage = null;
1235
+ this._notify();
1236
+ }
1237
+ // -----------------------------------------------------------------------
1238
+ // Reconnection
1239
+ // -----------------------------------------------------------------------
1240
+ _onDone() {
1241
+ this._connected = false;
1242
+ this._ws = null;
1243
+ if (this._intentionalDisconnect) {
1244
+ this._notify();
1245
+ return;
1246
+ }
1247
+ this._status = "disconnected" /* Disconnected */;
1248
+ this._notify();
1249
+ this._scheduleReconnect();
1250
+ }
1251
+ _onError() {
1252
+ if (this._intentionalDisconnect) return;
1253
+ this._status = "disconnected" /* Disconnected */;
1254
+ this._connected = false;
1255
+ this._notify();
1256
+ this._scheduleReconnect();
1257
+ }
1258
+ _scheduleReconnect() {
1259
+ if (this._intentionalDisconnect) return;
1260
+ if (this._reconnectAttempts >= _VocallClient.MAX_RECONNECT_ATTEMPTS) return;
1261
+ if (this._reconnectTimer) {
1262
+ clearTimeout(this._reconnectTimer);
1263
+ }
1264
+ const delaySec = Math.min(this._reconnectAttempts + 1, 15);
1265
+ this._reconnectAttempts++;
1266
+ this._reconnectTimer = setTimeout(() => this._doConnect(), delaySec * 1e3);
1267
+ }
1268
+ // -----------------------------------------------------------------------
1269
+ // Helpers
1270
+ // -----------------------------------------------------------------------
1271
+ _sleep(ms) {
1272
+ return new Promise((resolve) => setTimeout(resolve, ms));
1273
+ }
1274
+ // -----------------------------------------------------------------------
1275
+ // Dispose
1276
+ // -----------------------------------------------------------------------
1277
+ destroy() {
1278
+ this.disconnect(true);
1279
+ if (this._voice) {
1280
+ this._voice.dispose();
1281
+ this._voice = null;
1282
+ }
1283
+ this._listeners.clear();
1284
+ }
1285
+ };
1286
+ _VocallClient.MAX_RECONNECT_ATTEMPTS = 10;
1287
+ _VocallClient.RETRY_DELAY_MS = 300;
1288
+ _VocallClient.MAX_RETRIES = 3;
1289
+ // Sequential actions that must not run in parallel
1290
+ _VocallClient.SEQUENTIAL_ACTIONS = /* @__PURE__ */ new Set([
1291
+ "navigate",
1292
+ "click",
1293
+ "open_modal",
1294
+ "close_modal",
1295
+ "ask_confirm",
1296
+ "show_toast"
1297
+ ]);
1298
+ var VocallClient = _VocallClient;
1299
+
1300
+ // src/context/vocall-provider.tsx
1301
+ import { createContext, useContext, useEffect, useMemo, useRef } from "react";
1302
+ import { jsx } from "react/jsx-runtime";
1303
+ var VocallContext = createContext(null);
1304
+ function VocallProvider({ serverUrl, token, visitorId, children }) {
1305
+ const clientRef = useRef(null);
1306
+ const client = useMemo(() => {
1307
+ clientRef.current?.destroy();
1308
+ const c = new VocallClient(serverUrl, { token, visitorId });
1309
+ clientRef.current = c;
1310
+ return c;
1311
+ }, [serverUrl]);
1312
+ client.token = token;
1313
+ useEffect(() => {
1314
+ return () => {
1315
+ clientRef.current?.destroy();
1316
+ clientRef.current = null;
1317
+ };
1318
+ }, []);
1319
+ return /* @__PURE__ */ jsx(VocallContext.Provider, { value: client, children });
1320
+ }
1321
+ function useVocallClient() {
1322
+ const client = useContext(VocallContext);
1323
+ if (!client) {
1324
+ throw new Error("useVocallClient must be used within a <VocallProvider>");
1325
+ }
1326
+ return client;
1327
+ }
1328
+
1329
+ // src/hooks/use-vocall.ts
1330
+ import { useCallback, useSyncExternalStore } from "react";
1331
+ function useVocall() {
1332
+ const client = useVocallClient();
1333
+ const subscribe = useCallback(
1334
+ (onStoreChange) => client.subscribe(onStoreChange),
1335
+ [client]
1336
+ );
1337
+ const status = useSyncExternalStore(
1338
+ subscribe,
1339
+ () => client.status,
1340
+ () => "disconnected" /* Disconnected */
1341
+ );
1342
+ const connected = useSyncExternalStore(
1343
+ subscribe,
1344
+ () => client.connected,
1345
+ () => false
1346
+ );
1347
+ const messages = useSyncExternalStore(
1348
+ subscribe,
1349
+ () => client.messages,
1350
+ () => []
1351
+ );
1352
+ const sessionId = useSyncExternalStore(
1353
+ subscribe,
1354
+ () => client.sessionId,
1355
+ () => null
1356
+ );
1357
+ const voiceEnabled = useSyncExternalStore(
1358
+ subscribe,
1359
+ () => client.voiceEnabled,
1360
+ () => false
1361
+ );
1362
+ const voiceSupported = useSyncExternalStore(
1363
+ subscribe,
1364
+ () => client.voiceSupported,
1365
+ () => false
1366
+ );
1367
+ const voiceState = useSyncExternalStore(
1368
+ subscribe,
1369
+ () => client.voiceState,
1370
+ () => "idle"
1371
+ );
1372
+ const recording = useSyncExternalStore(
1373
+ subscribe,
1374
+ () => client.recording,
1375
+ () => false
1376
+ );
1377
+ const partialTranscription = useSyncExternalStore(
1378
+ subscribe,
1379
+ () => client.partialTranscription,
1380
+ () => null
1381
+ );
1382
+ const sendText = useCallback(
1383
+ (text) => client.sendText(text),
1384
+ [client]
1385
+ );
1386
+ const connect = useCallback(
1387
+ (manifest) => client.connect(manifest),
1388
+ [client]
1389
+ );
1390
+ const disconnect = useCallback(
1391
+ () => client.disconnect(),
1392
+ [client]
1393
+ );
1394
+ const clearMessages = useCallback(
1395
+ () => client.clearMessages(),
1396
+ [client]
1397
+ );
1398
+ return {
1399
+ client,
1400
+ status,
1401
+ messages,
1402
+ connected,
1403
+ sessionId,
1404
+ voiceEnabled,
1405
+ voiceSupported,
1406
+ voiceState,
1407
+ recording,
1408
+ partialTranscription,
1409
+ sendText,
1410
+ connect,
1411
+ disconnect,
1412
+ clearMessages
1413
+ };
1414
+ }
1415
+
1416
+ // src/hooks/use-vocall-field.ts
1417
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState } from "react";
1418
+ function useVocallField(screenId, fieldId, options) {
1419
+ const client = useVocallClient();
1420
+ const [registered, setRegistered] = useState(false);
1421
+ const elementRef = useRef2(null);
1422
+ const ref = useCallback2(
1423
+ (element) => {
1424
+ if (elementRef.current && registered) {
1425
+ client.unregisterField(screenId, fieldId);
1426
+ setRegistered(false);
1427
+ }
1428
+ elementRef.current = element;
1429
+ if (element) {
1430
+ const isInput = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
1431
+ client.registerField(screenId, fieldId, {
1432
+ element,
1433
+ setValue: options?.setValue ?? ((value) => {
1434
+ if (isInput) {
1435
+ element.value = value;
1436
+ element.dispatchEvent(new Event("input", { bubbles: true }));
1437
+ element.dispatchEvent(new Event("change", { bubbles: true }));
1438
+ }
1439
+ }),
1440
+ getValue: options?.getValue ?? (() => {
1441
+ if (isInput) {
1442
+ return element.value;
1443
+ }
1444
+ return "";
1445
+ })
1446
+ });
1447
+ setRegistered(true);
1448
+ }
1449
+ },
1450
+ [client, screenId, fieldId, options?.setValue, options?.getValue, registered]
1451
+ );
1452
+ useEffect2(() => {
1453
+ return () => {
1454
+ client.unregisterField(screenId, fieldId);
1455
+ };
1456
+ }, [client, screenId, fieldId]);
1457
+ return { ref, registered };
1458
+ }
1459
+
1460
+ // src/hooks/use-vocall-action.ts
1461
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
1462
+ function useVocallAction(screenId, actionId, callback) {
1463
+ const client = useVocallClient();
1464
+ const callbackRef = useRef3(callback);
1465
+ callbackRef.current = callback;
1466
+ useEffect3(() => {
1467
+ const handler = () => callbackRef.current();
1468
+ client.registerAction(screenId, actionId, handler);
1469
+ return () => {
1470
+ client.unregisterAction(screenId, actionId);
1471
+ };
1472
+ }, [client, screenId, actionId]);
1473
+ }
1474
+
1475
+ // src/hooks/use-vocall-voice.ts
1476
+ import { useCallback as useCallback3, useSyncExternalStore as useSyncExternalStore2 } from "react";
1477
+ function useVocallVoice() {
1478
+ const client = useVocallClient();
1479
+ const subscribe = useCallback3(
1480
+ (onStoreChange) => client.subscribe(onStoreChange),
1481
+ [client]
1482
+ );
1483
+ const voiceEnabled = useSyncExternalStore2(
1484
+ subscribe,
1485
+ () => client.voiceEnabled,
1486
+ () => false
1487
+ );
1488
+ const voiceState = useSyncExternalStore2(
1489
+ subscribe,
1490
+ () => client.voiceState,
1491
+ () => "idle"
1492
+ );
1493
+ const recording = useSyncExternalStore2(
1494
+ subscribe,
1495
+ () => client.recording,
1496
+ () => false
1497
+ );
1498
+ const partialTranscription = useSyncExternalStore2(
1499
+ subscribe,
1500
+ () => client.partialTranscription,
1501
+ () => null
1502
+ );
1503
+ const audioLevel = useSyncExternalStore2(
1504
+ subscribe,
1505
+ () => client.audioLevel,
1506
+ () => 0
1507
+ );
1508
+ const startAlwaysListening = useCallback3(
1509
+ () => client.startAlwaysListening(),
1510
+ [client]
1511
+ );
1512
+ const stopAlwaysListening = useCallback3(
1513
+ () => client.stopAlwaysListening(),
1514
+ [client]
1515
+ );
1516
+ const startRecording = useCallback3(
1517
+ () => client.startRecording(),
1518
+ [client]
1519
+ );
1520
+ const stopRecording = useCallback3(
1521
+ () => client.stopRecording(),
1522
+ [client]
1523
+ );
1524
+ const interrupt = useCallback3(
1525
+ () => client.interrupt(),
1526
+ [client]
1527
+ );
1528
+ return {
1529
+ voiceEnabled,
1530
+ voiceState,
1531
+ recording,
1532
+ partialTranscription,
1533
+ audioLevel,
1534
+ startAlwaysListening,
1535
+ stopAlwaysListening,
1536
+ startRecording,
1537
+ stopRecording,
1538
+ interrupt
1539
+ };
1540
+ }
1541
+
1542
+ // src/components/VocallChat.tsx
1543
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
1544
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1545
+ var STATUS_LABELS = {
1546
+ thinking: "Pensando...",
1547
+ executing: "Executando...",
1548
+ speaking: "Falando...",
1549
+ listening: "Ouvindo...",
1550
+ recording: "Gravando..."
1551
+ };
1552
+ function VocallChat({
1553
+ open,
1554
+ onClose,
1555
+ assistantName = "Emma",
1556
+ className,
1557
+ style
1558
+ }) {
1559
+ const { status, messages, connected, voiceSupported, recording, sendText, clearMessages, client } = useVocall();
1560
+ const [input, setInput] = useState2("");
1561
+ const [micActive, setMicActive] = useState2(false);
1562
+ const messagesEndRef = useRef4(null);
1563
+ useEffect4(() => {
1564
+ if (micActive && !recording) {
1565
+ setMicActive(false);
1566
+ }
1567
+ }, [micActive, recording]);
1568
+ const toggleMic = useCallback4(() => {
1569
+ if (micActive) {
1570
+ client.stopRecording();
1571
+ setMicActive(false);
1572
+ } else {
1573
+ client.startRecording();
1574
+ setMicActive(true);
1575
+ }
1576
+ }, [micActive, client]);
1577
+ useEffect4(() => {
1578
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1579
+ }, [messages]);
1580
+ const handleSend = useCallback4(() => {
1581
+ const text = input.trim();
1582
+ if (!text || !connected) return;
1583
+ sendText(text);
1584
+ setInput("");
1585
+ }, [input, connected, sendText]);
1586
+ const handleKeyDown = useCallback4(
1587
+ (e) => {
1588
+ if (e.key === "Enter" && !e.shiftKey) {
1589
+ e.preventDefault();
1590
+ handleSend();
1591
+ }
1592
+ },
1593
+ [handleSend]
1594
+ );
1595
+ if (!open) return null;
1596
+ const statusLabel = STATUS_LABELS[status] || "";
1597
+ const statusClass = status !== "idle" /* Idle */ ? status : "";
1598
+ return /* @__PURE__ */ jsxs(
1599
+ "div",
1600
+ {
1601
+ className: `vocall-chat-panel ${className || ""}`,
1602
+ style: {
1603
+ position: "fixed",
1604
+ bottom: 76,
1605
+ right: 16,
1606
+ width: 360,
1607
+ height: 560,
1608
+ background: "#fff",
1609
+ borderRadius: 16,
1610
+ boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
1611
+ display: "flex",
1612
+ flexDirection: "column",
1613
+ zIndex: 999,
1614
+ overflow: "hidden",
1615
+ ...style
1616
+ },
1617
+ children: [
1618
+ /* @__PURE__ */ jsxs(
1619
+ "div",
1620
+ {
1621
+ className: `vocall-chat-header ${statusClass}`,
1622
+ style: {
1623
+ padding: "8px 12px",
1624
+ background: "#1E293B",
1625
+ borderRadius: "16px 16px 0 0",
1626
+ display: "flex",
1627
+ alignItems: "center",
1628
+ gap: 8,
1629
+ borderBottom: `2px solid ${statusClass ? _statusBorderColor(status) : "transparent"}`,
1630
+ transition: "border-color 0.3s"
1631
+ },
1632
+ children: [
1633
+ /* @__PURE__ */ jsx2(
1634
+ "div",
1635
+ {
1636
+ style: {
1637
+ width: 8,
1638
+ height: 8,
1639
+ borderRadius: "50%",
1640
+ background: connected ? "#22C55E" : "#EF4444",
1641
+ boxShadow: connected ? "0 0 6px rgba(34,197,94,0.6)" : "none",
1642
+ flexShrink: 0
1643
+ }
1644
+ }
1645
+ ),
1646
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, fontWeight: 600, color: "#fff" }, children: assistantName }),
1647
+ statusLabel && /* @__PURE__ */ jsxs(Fragment, { children: [
1648
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, color: "#CBD5E1" }, children: "\xB7" }),
1649
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 500, color: _statusColor(status) }, children: statusLabel })
1650
+ ] }),
1651
+ /* @__PURE__ */ jsx2("div", { style: { flex: 1 } }),
1652
+ /* @__PURE__ */ jsx2(
1653
+ "button",
1654
+ {
1655
+ onClick: clearMessages,
1656
+ title: "Limpar",
1657
+ style: {
1658
+ width: 28,
1659
+ height: 28,
1660
+ display: "flex",
1661
+ alignItems: "center",
1662
+ justifyContent: "center",
1663
+ border: "none",
1664
+ background: "none",
1665
+ color: "#CBD5E1",
1666
+ cursor: "pointer",
1667
+ borderRadius: 6,
1668
+ fontSize: 14
1669
+ },
1670
+ children: "\u{1F5D1}"
1671
+ }
1672
+ ),
1673
+ /* @__PURE__ */ jsx2(
1674
+ "button",
1675
+ {
1676
+ onClick: onClose,
1677
+ title: "Fechar",
1678
+ style: {
1679
+ width: 28,
1680
+ height: 28,
1681
+ display: "flex",
1682
+ alignItems: "center",
1683
+ justifyContent: "center",
1684
+ border: "none",
1685
+ background: "none",
1686
+ color: "#CBD5E1",
1687
+ cursor: "pointer",
1688
+ borderRadius: 6,
1689
+ fontSize: 14
1690
+ },
1691
+ children: "\u2715"
1692
+ }
1693
+ )
1694
+ ]
1695
+ }
1696
+ ),
1697
+ /* @__PURE__ */ jsxs(
1698
+ "div",
1699
+ {
1700
+ style: {
1701
+ flex: 1,
1702
+ overflowY: "auto",
1703
+ padding: 12,
1704
+ display: "flex",
1705
+ flexDirection: "column",
1706
+ gap: 6
1707
+ },
1708
+ children: [
1709
+ messages.length === 0 ? /* @__PURE__ */ jsxs(
1710
+ "div",
1711
+ {
1712
+ style: {
1713
+ flex: 1,
1714
+ display: "flex",
1715
+ flexDirection: "column",
1716
+ alignItems: "center",
1717
+ justifyContent: "center",
1718
+ color: "#94A3B8",
1719
+ gap: 8
1720
+ },
1721
+ children: [
1722
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 32 }, children: "\u{1F4AC}" }),
1723
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13 }, children: connected ? `Diga algo para a ${assistantName}` : "Conecte-se para conversar" })
1724
+ ]
1725
+ }
1726
+ ) : messages.map((msg, i) => /* @__PURE__ */ jsx2(
1727
+ "div",
1728
+ {
1729
+ style: {
1730
+ display: "flex",
1731
+ maxWidth: 280,
1732
+ alignSelf: msg.role === "user" /* User */ ? "flex-end" : msg.role === "system" /* System */ ? "center" : "flex-start"
1733
+ },
1734
+ children: /* @__PURE__ */ jsx2(
1735
+ "div",
1736
+ {
1737
+ style: {
1738
+ padding: "8px 12px",
1739
+ borderRadius: 12,
1740
+ fontSize: 13,
1741
+ lineHeight: 1.5,
1742
+ wordBreak: "break-word",
1743
+ ..._bubbleStyle(msg.role)
1744
+ },
1745
+ children: msg.text
1746
+ }
1747
+ )
1748
+ },
1749
+ i
1750
+ )),
1751
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
1752
+ ]
1753
+ }
1754
+ ),
1755
+ /* @__PURE__ */ jsxs(
1756
+ "div",
1757
+ {
1758
+ style: {
1759
+ padding: "6px 8px 10px 12px",
1760
+ display: "flex",
1761
+ alignItems: "center",
1762
+ gap: 6,
1763
+ borderTop: "1px solid #E2E8F0"
1764
+ },
1765
+ children: [
1766
+ /* @__PURE__ */ jsx2(
1767
+ "input",
1768
+ {
1769
+ type: "text",
1770
+ value: input,
1771
+ onChange: (e) => setInput(e.target.value),
1772
+ onKeyDown: handleKeyDown,
1773
+ disabled: !connected || micActive,
1774
+ placeholder: micActive ? "Gravando... clique para parar" : "Digite uma mensagem...",
1775
+ style: {
1776
+ flex: 1,
1777
+ padding: "10px 12px",
1778
+ fontSize: 13,
1779
+ fontFamily: "inherit",
1780
+ border: "1px solid #E2E8F0",
1781
+ borderRadius: 10,
1782
+ background: "#F8FAFC",
1783
+ color: micActive ? "#FF4060" : "#1E293B",
1784
+ outline: "none",
1785
+ opacity: connected ? 1 : 0.5
1786
+ }
1787
+ }
1788
+ ),
1789
+ voiceSupported && /* @__PURE__ */ jsx2(
1790
+ "button",
1791
+ {
1792
+ onClick: toggleMic,
1793
+ disabled: !connected,
1794
+ style: {
1795
+ width: 36,
1796
+ height: 36,
1797
+ display: "flex",
1798
+ alignItems: "center",
1799
+ justifyContent: "center",
1800
+ border: "none",
1801
+ borderRadius: "50%",
1802
+ background: micActive ? "#FF4060" : "#3B82F6",
1803
+ color: "#fff",
1804
+ cursor: connected ? "pointer" : "not-allowed",
1805
+ flexShrink: 0
1806
+ },
1807
+ children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: micActive ? /* @__PURE__ */ jsx2("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) : /* @__PURE__ */ jsx2("path", { d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" }) })
1808
+ }
1809
+ ),
1810
+ /* @__PURE__ */ jsx2(
1811
+ "button",
1812
+ {
1813
+ onClick: handleSend,
1814
+ disabled: !connected || !input.trim() || micActive,
1815
+ style: {
1816
+ width: 36,
1817
+ height: 36,
1818
+ display: "flex",
1819
+ alignItems: "center",
1820
+ justifyContent: "center",
1821
+ border: "none",
1822
+ borderRadius: "50%",
1823
+ background: connected && input.trim() && !micActive ? "#3B82F6" : "#CBD5E1",
1824
+ color: "#fff",
1825
+ cursor: connected && input.trim() && !micActive ? "pointer" : "not-allowed",
1826
+ flexShrink: 0,
1827
+ fontSize: 16
1828
+ },
1829
+ children: "\u27A4"
1830
+ }
1831
+ )
1832
+ ]
1833
+ }
1834
+ )
1835
+ ]
1836
+ }
1837
+ );
1838
+ }
1839
+ function _bubbleStyle(role) {
1840
+ switch (role) {
1841
+ case "user" /* User */:
1842
+ return { background: "#3B82F6", color: "#fff" };
1843
+ case "agent" /* Agent */:
1844
+ return { background: "#F1F5F9", color: "#1E293B" };
1845
+ case "system" /* System */:
1846
+ return {
1847
+ background: "rgba(245,158,11,0.08)",
1848
+ color: "#F59E0B",
1849
+ fontSize: 11,
1850
+ fontStyle: "italic",
1851
+ textAlign: "center",
1852
+ maxWidth: "100%"
1853
+ };
1854
+ }
1855
+ }
1856
+ function _statusBorderColor(status) {
1857
+ switch (status) {
1858
+ case "thinking" /* Thinking */:
1859
+ return "#8B5CF6";
1860
+ case "executing" /* Executing */:
1861
+ return "#F59E0B";
1862
+ case "speaking" /* Speaking */:
1863
+ return "#FFB347";
1864
+ case "listening" /* Listening */:
1865
+ return "#00D4FF";
1866
+ case "recording" /* Recording */:
1867
+ return "#FF4060";
1868
+ default:
1869
+ return "transparent";
1870
+ }
1871
+ }
1872
+ function _statusColor(status) {
1873
+ switch (status) {
1874
+ case "thinking" /* Thinking */:
1875
+ return "#8B5CF6";
1876
+ case "executing" /* Executing */:
1877
+ return "#F59E0B";
1878
+ case "speaking" /* Speaking */:
1879
+ return "#FFB347";
1880
+ case "listening" /* Listening */:
1881
+ return "#00D4FF";
1882
+ case "recording" /* Recording */:
1883
+ return "#FF4060";
1884
+ default:
1885
+ return "#94A3B8";
1886
+ }
1887
+ }
1888
+
1889
+ // src/components/VocallFab.tsx
1890
+ import { jsx as jsx3 } from "react/jsx-runtime";
1891
+ var FAB_COLORS = {
1892
+ disconnected: { bg: "#94A3B8", shadow: "rgba(148,163,184,0.3)" },
1893
+ idle: { bg: "#3B82F6", shadow: "rgba(59,130,246,0.35)" },
1894
+ thinking: { bg: "#8B5CF6", shadow: "rgba(139,92,246,0.4)" },
1895
+ executing: { bg: "#F59E0B", shadow: "rgba(245,158,11,0.4)" },
1896
+ speaking: { bg: "#FFB347", shadow: "rgba(255,179,71,0.4)" },
1897
+ listening: { bg: "#00D4FF", shadow: "rgba(0,212,255,0.4)" },
1898
+ recording: { bg: "#FF4060", shadow: "rgba(255,64,96,0.4)" }
1899
+ };
1900
+ function VocallFab({ onClick, className, style }) {
1901
+ const { status, connected } = useVocall();
1902
+ const key = !connected ? "disconnected" : status;
1903
+ const colors = FAB_COLORS[key] ?? FAB_COLORS.idle;
1904
+ const isSpinner = status === "thinking" /* Thinking */ || status === "executing" /* Executing */;
1905
+ const isVoiceActive = status === "listening" /* Listening */ || status === "recording" /* Recording */ || status === "speaking" /* Speaking */;
1906
+ return /* @__PURE__ */ jsx3(
1907
+ "button",
1908
+ {
1909
+ className: `vocall-fab ${className || ""}`,
1910
+ onClick,
1911
+ style: {
1912
+ position: "fixed",
1913
+ bottom: 16,
1914
+ right: 16,
1915
+ width: 52,
1916
+ height: 52,
1917
+ borderRadius: "50%",
1918
+ background: colors.bg,
1919
+ color: "#fff",
1920
+ border: "none",
1921
+ cursor: "pointer",
1922
+ display: "flex",
1923
+ alignItems: "center",
1924
+ justifyContent: "center",
1925
+ boxShadow: `0 4px 12px ${colors.shadow}`,
1926
+ transition: "all 0.3s ease-in-out",
1927
+ zIndex: 1e3,
1928
+ fontSize: 24,
1929
+ ...style
1930
+ },
1931
+ children: isSpinner ? /* @__PURE__ */ jsx3(
1932
+ "div",
1933
+ {
1934
+ style: {
1935
+ width: 24,
1936
+ height: 24,
1937
+ border: "2px solid rgba(255,255,255,0.3)",
1938
+ borderTopColor: "#fff",
1939
+ borderRadius: "50%",
1940
+ animation: "vocall-spin 0.8s linear infinite"
1941
+ }
1942
+ }
1943
+ ) : isVoiceActive ? /* @__PURE__ */ jsx3("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx3("path", { d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" }) }) : /* @__PURE__ */ jsx3("span", { children: "\u{1F916}" })
1944
+ }
1945
+ );
1946
+ }
1947
+
1948
+ // src/components/VocallStatus.tsx
1949
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1950
+ var STATUS_CONFIG = {
1951
+ thinking: { label: "Pensando...", bg: "rgba(139,92,246,0.12)", color: "#8B5CF6" },
1952
+ executing: { label: "Executando...", bg: "rgba(245,158,11,0.12)", color: "#F59E0B" },
1953
+ speaking: { label: "Falando...", bg: "rgba(255,179,71,0.12)", color: "#FFB347" },
1954
+ listening: { label: "Ouvindo...", bg: "rgba(0,212,255,0.12)", color: "#00D4FF" },
1955
+ recording: { label: "Gravando...", bg: "rgba(239,68,68,0.12)", color: "#FF4060" }
1956
+ };
1957
+ function VocallStatusPill({ className, style }) {
1958
+ const { status, connected } = useVocall();
1959
+ const config = STATUS_CONFIG[status];
1960
+ if (!config || !connected || status === "idle" /* Idle */ || status === "disconnected" /* Disconnected */) {
1961
+ return null;
1962
+ }
1963
+ return /* @__PURE__ */ jsxs2(
1964
+ "div",
1965
+ {
1966
+ className: `vocall-status-pill ${className || ""}`,
1967
+ style: {
1968
+ display: "inline-flex",
1969
+ alignItems: "center",
1970
+ gap: 6,
1971
+ padding: "4px 10px",
1972
+ borderRadius: 12,
1973
+ fontSize: 11,
1974
+ fontWeight: 500,
1975
+ background: config.bg,
1976
+ color: config.color,
1977
+ ...style
1978
+ },
1979
+ children: [
1980
+ /* @__PURE__ */ jsx4(
1981
+ "div",
1982
+ {
1983
+ style: {
1984
+ width: 12,
1985
+ height: 12,
1986
+ border: "1.5px solid currentColor",
1987
+ borderTopColor: "transparent",
1988
+ borderRadius: "50%",
1989
+ animation: "vocall-spin 0.8s linear infinite"
1990
+ }
1991
+ }
1992
+ ),
1993
+ /* @__PURE__ */ jsx4("span", { children: config.label })
1994
+ ]
1995
+ }
1996
+ );
1997
+ }
1998
+ export {
1999
+ ChatRole,
2000
+ FieldType,
2001
+ FrameSplitter,
2002
+ VocallChat,
2003
+ VocallClient,
2004
+ VocallFab,
2005
+ VocallProvider,
2006
+ VocallStatus,
2007
+ VocallStatusPill,
2008
+ WebVoiceService,
2009
+ useVocall,
2010
+ useVocallAction,
2011
+ useVocallClient,
2012
+ useVocallField,
2013
+ useVocallVoice
2014
+ };
2015
+ //# sourceMappingURL=index.js.map