@edvak99/voice-receptionist-widget 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +37 -0
  2. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs +306 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs.map +1 -0
  4. package/fesm2022/{edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-DokIb-9r.mjs} +2 -2
  5. package/fesm2022/{edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map → edvak99-voice-receptionist-widget-ai-receptionist-api.service-DokIb-9r.mjs.map} +1 -1
  6. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs +277 -0
  7. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs.map +1 -0
  8. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs +416 -0
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs.map +1 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs +193 -0
  11. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs.map +1 -0
  12. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-Pla2UgeZ.mjs +111 -0
  13. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-Pla2UgeZ.mjs.map +1 -0
  14. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs → edvak99-voice-receptionist-widget-call-review.service-Di7GLG9k.mjs} +2 -2
  15. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map → edvak99-voice-receptionist-widget-call-review.service-Di7GLG9k.mjs.map} +1 -1
  16. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs +439 -0
  17. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs.map +1 -0
  18. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-D2pelzu4.mjs} +3 -3
  19. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map → edvak99-voice-receptionist-widget-dashboard-socket.service-D2pelzu4.mjs.map} +1 -1
  20. package/fesm2022/{edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs → edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs} +5 -5
  21. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs.map +1 -0
  22. package/fesm2022/{edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-Ig0Eoq1z.mjs} +6 -6
  23. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-Ig0Eoq1z.mjs.map +1 -0
  24. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs +81 -0
  25. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs.map +1 -0
  26. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs +1059 -0
  27. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs.map +1 -0
  28. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs +48 -0
  29. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs.map +1 -0
  30. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs +267 -0
  31. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs.map +1 -0
  32. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs +967 -0
  33. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs.map +1 -0
  34. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs +316 -0
  35. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs.map +1 -0
  36. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs +46 -0
  37. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs.map +1 -0
  38. package/fesm2022/edvak99-voice-receptionist-widget.mjs +228 -1
  39. package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
  40. package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +1 -2
  41. package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +0 -3
  42. package/package.json +1 -1
  43. package/public-api.d.ts +3 -0
  44. package/styles/chat-ui.scss +0 -1
  45. package/styles/eds.scss +0 -10
  46. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +0 -307
  47. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +0 -1
  48. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +0 -278
  49. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +0 -1
  50. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +0 -418
  51. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +0 -1
  52. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +0 -194
  53. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +0 -1
  54. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs +0 -111
  55. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map +0 -1
  56. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +0 -440
  57. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +0 -1
  58. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +0 -1
  59. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +0 -27
  60. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +0 -1
  61. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +0 -1
  62. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +0 -203
  63. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +0 -1
  64. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +0 -81
  65. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +0 -1
  66. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +0 -1070
  67. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +0 -1
  68. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +0 -48
  69. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +0 -1
  70. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +0 -268
  71. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +0 -1
  72. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +0 -968
  73. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +0 -1
  74. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs +0 -317
  75. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +0 -1
  76. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs +0 -47
  77. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map +0 -1
@@ -0,0 +1,1059 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, Injectable, input, output, computed, effect, ViewChild, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { ActivatedRoute, RouterLink } from '@angular/router';
5
+ import * as i1 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import { firstValueFrom } from 'rxjs';
8
+ import { VOICE_RECEPTIONIST_CONFIG, EdsIconComponent, ToastService } from './edvak99-voice-receptionist-widget.mjs';
9
+ import { e as resolveToolKey, f as resolveToolDisplayName, i as isUnknownCaller, A as AiReceptionistApiService, g as toolEventTrackKey, c as callerMetaLine, d as ageFromDob } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-DokIb-9r.mjs';
10
+ import { D as DashboardSocketService } from './edvak99-voice-receptionist-widget-dashboard-socket.service-D2pelzu4.mjs';
11
+ import { C as CallRecordingPlayerComponent } from './edvak99-voice-receptionist-widget-call-recording-player.component-Pla2UgeZ.mjs';
12
+ import { Track, Room, RoomEvent, ConnectionState } from 'livekit-client';
13
+ import { E as EdsElapsedPipe } from './edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs';
14
+ import { E as EdsAvatarComponent } from './edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs';
15
+
16
+ function mapChunkToLine(chunk) {
17
+ const at = Date.parse(chunk.timestamp);
18
+ return {
19
+ who: chunk.role === 'staff'
20
+ ? 'staff'
21
+ : chunk.role === 'system'
22
+ ? 'system'
23
+ : chunk.role === 'assistant'
24
+ ? 'ai'
25
+ : 'patient',
26
+ text: chunk.text,
27
+ at: Number.isFinite(at) ? at : Date.now(),
28
+ };
29
+ }
30
+ function formatEventTime(iso) {
31
+ return new Date(iso).toLocaleTimeString('en-US', {
32
+ hour: 'numeric',
33
+ minute: '2-digit',
34
+ second: '2-digit',
35
+ hour12: false,
36
+ });
37
+ }
38
+ function mapCallEventToTool(event) {
39
+ const p = event.payload;
40
+ const state = p['state'];
41
+ const rawName = String(p['name'] ?? event.eventType.replace(/^tool_/, ''));
42
+ const tech = String(p['tech'] ?? event.eventType);
43
+ const key = resolveToolKey(p['key'] != null ? String(p['key']) : undefined, rawName, tech);
44
+ return {
45
+ t: formatEventTime(event.occurredAt),
46
+ key,
47
+ name: resolveToolDisplayName(key, rawName),
48
+ tech,
49
+ result: String(p['result'] ?? ''),
50
+ state: state === 'ok' || state === 'warn' || state === 'error' || state === 'active'
51
+ ? state
52
+ : 'ok',
53
+ };
54
+ }
55
+ function mapLifecycleEventToTool(event) {
56
+ const p = event.payload;
57
+ return {
58
+ t: formatEventTime(event.occurredAt),
59
+ name: event.eventType.replace(/_/g, ' '),
60
+ tech: event.eventType,
61
+ result: String(p['detail'] ?? p['message'] ?? p['result'] ?? ''),
62
+ state: 'ok',
63
+ };
64
+ }
65
+ function isDuplicateTranscriptLine(lines, line) {
66
+ return lines.some((l) => l.who === line.who && l.text.trim() === line.text.trim());
67
+ }
68
+
69
+ /**
70
+ * Live call stream over shared Socket.io (`/dashboard` namespace).
71
+ * REST seeds transcript/tools once; socket delivers `new_chunk` + `call_event`.
72
+ */
73
+ class LiveTranscriptService {
74
+ constructor() {
75
+ this.socket = inject(DashboardSocketService);
76
+ this.config = inject(VOICE_RECEPTIONIST_CONFIG);
77
+ this.lines = signal([]);
78
+ this.patientInterim = signal(null);
79
+ this.aiInterim = signal(null);
80
+ this.staffInterim = signal(null);
81
+ this.tools = signal([]);
82
+ this.connected = this.socket.connected;
83
+ this.error = signal(null);
84
+ this.callEnded = signal(false);
85
+ this.summaryRequested = signal(false);
86
+ this.activeCallId = null;
87
+ this.watching = false;
88
+ this.mockTimer = null;
89
+ this.mockIndex = 0;
90
+ this.chunkHandler = (chunk) => this.handleChunk(chunk);
91
+ this.eventHandler = (event) => this.handleCallEvent(event);
92
+ this.onSocketConnect = () => {
93
+ if (this.activeCallId) {
94
+ this.socket.emit('watch_call', { callId: this.activeCallId });
95
+ }
96
+ };
97
+ }
98
+ seed(transcript, toolEvents) {
99
+ this.lines.set(transcript.map((line) => ({ ...line })));
100
+ this.tools.set(toolEvents.map((tool) => ({ ...tool })));
101
+ }
102
+ watch(callId, live = true) {
103
+ const id = callId.trim();
104
+ if (!id || !live)
105
+ return;
106
+ const switchingCall = this.activeCallId !== id;
107
+ if (switchingCall) {
108
+ this.unsubscribeSocketEvents();
109
+ this.clearInterim();
110
+ this.callEnded.set(false);
111
+ this.summaryRequested.set(false);
112
+ this.activeCallId = id;
113
+ }
114
+ else if (this.watching) {
115
+ return;
116
+ }
117
+ if (this.config.useMockApi) {
118
+ this.watching = true;
119
+ this.startMockStream(id);
120
+ return;
121
+ }
122
+ if (!this.watching) {
123
+ this.socket.acquire();
124
+ this.socket.on('new_chunk', this.chunkHandler);
125
+ this.socket.on('call_event', this.eventHandler);
126
+ this.socket.onConnect(this.onSocketConnect);
127
+ this.watching = true;
128
+ }
129
+ if (this.socket.connected()) {
130
+ this.socket.emit('watch_call', { callId: id });
131
+ }
132
+ }
133
+ stop() {
134
+ this.activeCallId = null;
135
+ this.unsubscribeSocketEvents();
136
+ this.resetStreamState();
137
+ }
138
+ markCompleted() {
139
+ this.callEnded.set(true);
140
+ this.unsubscribeSocketEvents();
141
+ this.clearInterim();
142
+ }
143
+ unsubscribeSocketEvents() {
144
+ this.stopMockStream();
145
+ if (!this.watching)
146
+ return;
147
+ this.socket.off('new_chunk', this.chunkHandler);
148
+ this.socket.off('call_event', this.eventHandler);
149
+ this.socket.offConnect(this.onSocketConnect);
150
+ this.socket.release();
151
+ this.watching = false;
152
+ }
153
+ handleChunk(chunk) {
154
+ if (!chunk?.text?.trim())
155
+ return;
156
+ if (chunk.role === 'user') {
157
+ if (chunk.isFinal) {
158
+ this.patientInterim.set(null);
159
+ this.appendLineIfNew(mapChunkToLine(chunk));
160
+ }
161
+ else {
162
+ this.patientInterim.set(chunk.text);
163
+ }
164
+ return;
165
+ }
166
+ if (chunk.role === 'system') {
167
+ this.appendLineIfNew(mapChunkToLine(chunk));
168
+ return;
169
+ }
170
+ if (chunk.role === 'staff') {
171
+ if (chunk.isFinal) {
172
+ this.staffInterim.set(null);
173
+ this.appendLineIfNew(mapChunkToLine(chunk));
174
+ }
175
+ else {
176
+ this.staffInterim.set(chunk.text);
177
+ }
178
+ return;
179
+ }
180
+ if (chunk.isFinal) {
181
+ this.aiInterim.set(null);
182
+ this.appendLineIfNew(mapChunkToLine(chunk));
183
+ }
184
+ else {
185
+ this.aiInterim.set(chunk.text);
186
+ }
187
+ }
188
+ handleCallEvent(event) {
189
+ if (!event?.eventType)
190
+ return;
191
+ if (event.eventType.startsWith('tool_')) {
192
+ this.tools.update((prev) => [...prev, mapCallEventToTool(event)]);
193
+ return;
194
+ }
195
+ if (event.eventType === 'call_completed') {
196
+ this.markCompleted();
197
+ return;
198
+ }
199
+ if (event.eventType === 'summary_generated') {
200
+ this.summaryRequested.set(true);
201
+ return;
202
+ }
203
+ this.tools.update((prev) => [...prev, mapLifecycleEventToTool(event)]);
204
+ }
205
+ appendLineIfNew(line) {
206
+ this.lines.update((prev) => {
207
+ if (isDuplicateTranscriptLine(prev, line))
208
+ return prev;
209
+ return [...prev, line];
210
+ });
211
+ }
212
+ startMockStream(callId) {
213
+ this.stopMockStream();
214
+ this.mockIndex = 0;
215
+ const mockChunks = [
216
+ {
217
+ role: 'user',
218
+ text: 'Hi, I need to schedule an appointment.',
219
+ isFinal: false,
220
+ timestamp: new Date().toISOString(),
221
+ },
222
+ {
223
+ role: 'user',
224
+ text: 'Hi, I need to schedule an appointment.',
225
+ isFinal: true,
226
+ timestamp: new Date().toISOString(),
227
+ },
228
+ {
229
+ role: 'assistant',
230
+ text: 'Of course — I can help with that. Which provider would you prefer?',
231
+ isFinal: true,
232
+ timestamp: new Date().toISOString(),
233
+ },
234
+ ];
235
+ const mockEvents = [
236
+ {
237
+ eventType: 'tool_check_doctor_availability',
238
+ occurredAt: new Date().toISOString(),
239
+ payload: {
240
+ key: 'check_doctor_availability',
241
+ name: 'Checked Doctor Availability',
242
+ tech: 'tool_check_doctor_availability',
243
+ result: 'Found 3 available time slots',
244
+ state: 'ok',
245
+ },
246
+ },
247
+ ];
248
+ this.mockTimer = setInterval(() => {
249
+ if (this.activeCallId !== callId) {
250
+ this.stopMockStream();
251
+ return;
252
+ }
253
+ if (this.mockIndex < mockChunks.length) {
254
+ this.handleChunk(mockChunks[this.mockIndex]);
255
+ this.mockIndex++;
256
+ return;
257
+ }
258
+ if (this.mockIndex === mockChunks.length) {
259
+ this.handleCallEvent(mockEvents[0]);
260
+ this.mockIndex++;
261
+ }
262
+ }, 1500);
263
+ }
264
+ stopMockStream() {
265
+ if (this.mockTimer) {
266
+ clearInterval(this.mockTimer);
267
+ this.mockTimer = null;
268
+ }
269
+ }
270
+ clearInterim() {
271
+ this.patientInterim.set(null);
272
+ this.aiInterim.set(null);
273
+ this.staffInterim.set(null);
274
+ }
275
+ resetStreamState() {
276
+ this.lines.set([]);
277
+ this.tools.set([]);
278
+ this.clearInterim();
279
+ this.error.set(null);
280
+ this.callEnded.set(false);
281
+ this.summaryRequested.set(false);
282
+ }
283
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
284
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptService, providedIn: 'root' }); }
285
+ }
286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptService, decorators: [{
287
+ type: Injectable,
288
+ args: [{ providedIn: 'root' }]
289
+ }] });
290
+
291
+ class LiveTranscriptPanelComponent {
292
+ constructor() {
293
+ this.live = input(true);
294
+ this.paused = input(false);
295
+ this.humanTakeover = input(false);
296
+ this.showStaffInput = input(false);
297
+ this.staffMessage = input('');
298
+ /** Resolved patient display name from the live call. */
299
+ this.callerName = input(null);
300
+ /** Whether the caller was matched to a patient chart. */
301
+ this.callerMatched = input(false);
302
+ this.staffMessageChange = output();
303
+ this.sendMessage = output();
304
+ this.stream = inject(LiveTranscriptService);
305
+ this.lines = this.stream.lines;
306
+ this.patientInterim = this.stream.patientInterim;
307
+ this.aiInterim = this.stream.aiInterim;
308
+ this.staffInterim = this.stream.staffInterim;
309
+ this.connected = this.stream.connected;
310
+ this.error = this.stream.error;
311
+ this.callerLabel = computed(() => {
312
+ const name = this.callerName()?.trim() ?? '';
313
+ if (isUnknownCaller(name, this.callerMatched()))
314
+ return 'Unknown caller';
315
+ return name || 'Unknown caller';
316
+ });
317
+ this.shouldScroll = false;
318
+ effect(() => {
319
+ this.lines();
320
+ this.patientInterim();
321
+ this.aiInterim();
322
+ this.staffInterim();
323
+ this.requestScroll();
324
+ });
325
+ }
326
+ ngAfterViewChecked() {
327
+ if (this.shouldScroll && this.scrollHost) {
328
+ const el = this.scrollHost.nativeElement;
329
+ el.scrollTop = el.scrollHeight;
330
+ this.shouldScroll = false;
331
+ }
332
+ }
333
+ roleLabel(who) {
334
+ switch (who) {
335
+ case 'ai':
336
+ return 'Ava · AI';
337
+ case 'patient':
338
+ return this.callerLabel();
339
+ case 'staff':
340
+ return 'Staff';
341
+ default:
342
+ return 'System';
343
+ }
344
+ }
345
+ speakerIcon(who) {
346
+ switch (who) {
347
+ case 'ai':
348
+ return 'graphic_eq';
349
+ case 'staff':
350
+ return 'support_agent';
351
+ case 'patient':
352
+ return 'person';
353
+ default:
354
+ return 'info';
355
+ }
356
+ }
357
+ systemIcon(flag) {
358
+ if (flag === 'verify')
359
+ return 'verified_user';
360
+ if (flag === 'fail' || flag === 'escalate')
361
+ return 'bolt';
362
+ return 'info';
363
+ }
364
+ onStaffInput(value) {
365
+ this.staffMessageChange.emit(value);
366
+ }
367
+ onSend() {
368
+ this.sendMessage.emit();
369
+ }
370
+ requestScroll() {
371
+ this.shouldScroll = true;
372
+ }
373
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
374
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: LiveTranscriptPanelComponent, isStandalone: true, selector: "eds-live-transcript-panel", inputs: { live: { classPropertyName: "live", publicName: "live", isSignal: true, isRequired: false, transformFunction: null }, paused: { classPropertyName: "paused", publicName: "paused", isSignal: true, isRequired: false, transformFunction: null }, humanTakeover: { classPropertyName: "humanTakeover", publicName: "humanTakeover", isSignal: true, isRequired: false, transformFunction: null }, showStaffInput: { classPropertyName: "showStaffInput", publicName: "showStaffInput", isSignal: true, isRequired: false, transformFunction: null }, staffMessage: { classPropertyName: "staffMessage", publicName: "staffMessage", isSignal: true, isRequired: false, transformFunction: null }, callerName: { classPropertyName: "callerName", publicName: "callerName", isSignal: true, isRequired: false, transformFunction: null }, callerMatched: { classPropertyName: "callerMatched", publicName: "callerMatched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { staffMessageChange: "staffMessageChange", sendMessage: "sendMessage" }, host: { classAttribute: "flex h-full min-h-0 flex-1 flex-col max-[900px]:h-auto max-[900px]:max-h-none max-[900px]:min-h-[360px]" }, viewQueries: [{ propertyName: "scrollHost", first: true, predicate: ["scrollHost"], descendants: true }], ngImport: i0, template: "<section class=\"flex h-full min-h-0 flex-1 flex-col rounded-xl border border-zinc-200 bg-white max-[900px]:h-auto max-[900px]:max-h-none max-[900px]:min-h-[360px]\" aria-labelledby=\"live-transcript-title\">\n <header class=\"flex shrink-0 items-center justify-between gap-3 border-b border-zinc-200 px-4.5 py-3.5\">\n <h2 id=\"live-transcript-title\" class=\"m-0 font-mono text-xs font-semibold uppercase tracking-wider text-zinc-500\">Live transcript</h2>\n </header>\n\n <div #scrollHost class=\"flex flex-1 flex-col gap-3.5 overflow-auto bg-zinc-50 p-4.5\" role=\"log\" aria-live=\"polite\">\n @for (line of lines(); track $index) {\n @if (line.who === 'system') {\n <div\n class=\"inline-flex max-w-[90%] items-center gap-1.5 self-center rounded-full border px-3 py-1.5 text-center text-xs font-medium eds-animate-fade-up\"\n [class]=\"line.flag === 'verify' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : line.flag === 'fail' || line.flag === 'escalate' ? 'border-amber-200 bg-amber-50 text-amber-700' : 'border-zinc-200 bg-white text-zinc-600'\"\n >\n <eds-icon [name]=\"systemIcon(line.flag)\" [size]=\"14\" />\n <span>{{ line.text }}</span>\n </div>\n } @else {\n <article\n class=\"flex max-w-[72%] gap-2.5 eds-animate-fade-up\"\n [class]=\"line.who === 'patient' ? 'mr-auto self-start' : 'ml-auto flex-row-reverse self-end'\"\n >\n <div\n class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full\"\n [class]=\"line.who === 'ai' ? 'live-transcript-panel__avatar--ai text-white' : line.who === 'staff' ? 'border border-violet-200 bg-violet-50 text-violet-700' : 'bg-zinc-200 text-zinc-600'\"\n >\n <eds-icon [name]=\"speakerIcon(line.who)\" [size]=\"16\" />\n </div>\n <div\n class=\"flex min-w-0 flex-col gap-1\"\n [class]=\"line.who === 'patient' ? 'items-start' : 'items-end'\"\n >\n <span\n class=\"font-mono text-xs font-semibold tracking-wide\"\n [class]=\"line.who === 'ai' ? 'text-blue-600' : line.who === 'staff' ? 'text-violet-700' : 'text-zinc-400'\"\n >{{ roleLabel(line.who) }}</span>\n <div\n class=\"px-3.5 py-2\"\n [class]=\"line.who === 'patient' ? 'rounded-tl rounded-tr-xl rounded-br-xl rounded-bl-xl border border-zinc-200 bg-white' : line.who === 'staff' ? 'rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-violet-200 bg-violet-50' : 'rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-blue-200 bg-blue-50'\"\n >\n <p class=\"m-0 text-sm leading-snug\" [class]=\"line.who === 'staff' ? 'text-violet-900' : 'text-zinc-900'\">{{ line.text }}</p>\n </div>\n </div>\n </article>\n }\n }\n\n @if (patientInterim(); as interim) {\n <article class=\"mr-auto flex max-w-[72%] gap-2.5 self-start eds-animate-fade-up\">\n <div class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-zinc-600\">\n <eds-icon name=\"person\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-start gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-zinc-400\">{{ callerLabel() }} \u00B7 listening</span>\n <div class=\"rounded-tl rounded-tr-xl rounded-br-xl rounded-bl-xl border border-zinc-200 bg-white px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-zinc-900\">{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (staffInterim(); as interim) {\n <article class=\"ml-auto flex max-w-[72%] flex-row-reverse gap-2.5 self-end eds-animate-fade-up\">\n <div class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-violet-200 bg-violet-50 text-violet-700\">\n <eds-icon name=\"support_agent\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-end gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-violet-700\">Staff \u00B7 speaking</span>\n <div class=\"rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-violet-200 bg-violet-50 px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-violet-900\">{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (aiInterim(); as interim) {\n <article class=\"ml-auto flex max-w-[72%] flex-row-reverse gap-2.5 self-end eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar--ai flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white\">\n <eds-icon name=\"graphic_eq\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-end gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-blue-600\">Ava \u00B7 AI</span>\n <div class=\"rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-blue-200 bg-blue-50 px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-zinc-900\">{{ interim }}<span class=\"live-transcript-panel__caret\" aria-hidden=\"true\"></span></p>\n </div>\n </div>\n </article>\n }\n\n @if (lines().length === 0 && !patientInterim() && !aiInterim() && !staffInterim()) {\n <p class=\"m-0 px-3 py-8 text-center text-sm leading-snug text-zinc-500\">Waiting for conversation\u2026</p>\n }\n </div>\n\n @if (showStaffInput()) {\n <footer class=\"flex shrink-0 gap-2 border-t border-zinc-200 bg-white p-3\">\n <input\n type=\"text\"\n class=\"h-9 flex-1 rounded-md border border-zinc-200 bg-white px-3.5 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none\"\n placeholder=\"Type to speak as staff\u2026\"\n [ngModel]=\"staffMessage()\"\n (ngModelChange)=\"onStaffInput($event)\"\n (keyup.enter)=\"onSend()\"\n autocomplete=\"off\"\n />\n </footer>\n }\n</section>\n", styles: [".live-transcript-panel__avatar--ai{background:linear-gradient(135deg,#3b82f6,#8b5cf6)}.live-transcript-panel__caret{display:inline-block;width:7px;height:14px;margin-left:2px;vertical-align:text-bottom;background:#3b82f6;animation:eds-caret 1s step-end infinite}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
375
+ }
376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptPanelComponent, decorators: [{
377
+ type: Component,
378
+ args: [{ selector: 'eds-live-transcript-panel', imports: [FormsModule, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
379
+ class: 'flex h-full min-h-0 flex-1 flex-col max-[900px]:h-auto max-[900px]:max-h-none max-[900px]:min-h-[360px]',
380
+ }, template: "<section class=\"flex h-full min-h-0 flex-1 flex-col rounded-xl border border-zinc-200 bg-white max-[900px]:h-auto max-[900px]:max-h-none max-[900px]:min-h-[360px]\" aria-labelledby=\"live-transcript-title\">\n <header class=\"flex shrink-0 items-center justify-between gap-3 border-b border-zinc-200 px-4.5 py-3.5\">\n <h2 id=\"live-transcript-title\" class=\"m-0 font-mono text-xs font-semibold uppercase tracking-wider text-zinc-500\">Live transcript</h2>\n </header>\n\n <div #scrollHost class=\"flex flex-1 flex-col gap-3.5 overflow-auto bg-zinc-50 p-4.5\" role=\"log\" aria-live=\"polite\">\n @for (line of lines(); track $index) {\n @if (line.who === 'system') {\n <div\n class=\"inline-flex max-w-[90%] items-center gap-1.5 self-center rounded-full border px-3 py-1.5 text-center text-xs font-medium eds-animate-fade-up\"\n [class]=\"line.flag === 'verify' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : line.flag === 'fail' || line.flag === 'escalate' ? 'border-amber-200 bg-amber-50 text-amber-700' : 'border-zinc-200 bg-white text-zinc-600'\"\n >\n <eds-icon [name]=\"systemIcon(line.flag)\" [size]=\"14\" />\n <span>{{ line.text }}</span>\n </div>\n } @else {\n <article\n class=\"flex max-w-[72%] gap-2.5 eds-animate-fade-up\"\n [class]=\"line.who === 'patient' ? 'mr-auto self-start' : 'ml-auto flex-row-reverse self-end'\"\n >\n <div\n class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full\"\n [class]=\"line.who === 'ai' ? 'live-transcript-panel__avatar--ai text-white' : line.who === 'staff' ? 'border border-violet-200 bg-violet-50 text-violet-700' : 'bg-zinc-200 text-zinc-600'\"\n >\n <eds-icon [name]=\"speakerIcon(line.who)\" [size]=\"16\" />\n </div>\n <div\n class=\"flex min-w-0 flex-col gap-1\"\n [class]=\"line.who === 'patient' ? 'items-start' : 'items-end'\"\n >\n <span\n class=\"font-mono text-xs font-semibold tracking-wide\"\n [class]=\"line.who === 'ai' ? 'text-blue-600' : line.who === 'staff' ? 'text-violet-700' : 'text-zinc-400'\"\n >{{ roleLabel(line.who) }}</span>\n <div\n class=\"px-3.5 py-2\"\n [class]=\"line.who === 'patient' ? 'rounded-tl rounded-tr-xl rounded-br-xl rounded-bl-xl border border-zinc-200 bg-white' : line.who === 'staff' ? 'rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-violet-200 bg-violet-50' : 'rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-blue-200 bg-blue-50'\"\n >\n <p class=\"m-0 text-sm leading-snug\" [class]=\"line.who === 'staff' ? 'text-violet-900' : 'text-zinc-900'\">{{ line.text }}</p>\n </div>\n </div>\n </article>\n }\n }\n\n @if (patientInterim(); as interim) {\n <article class=\"mr-auto flex max-w-[72%] gap-2.5 self-start eds-animate-fade-up\">\n <div class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-zinc-600\">\n <eds-icon name=\"person\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-start gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-zinc-400\">{{ callerLabel() }} \u00B7 listening</span>\n <div class=\"rounded-tl rounded-tr-xl rounded-br-xl rounded-bl-xl border border-zinc-200 bg-white px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-zinc-900\">{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (staffInterim(); as interim) {\n <article class=\"ml-auto flex max-w-[72%] flex-row-reverse gap-2.5 self-end eds-animate-fade-up\">\n <div class=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-violet-200 bg-violet-50 text-violet-700\">\n <eds-icon name=\"support_agent\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-end gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-violet-700\">Staff \u00B7 speaking</span>\n <div class=\"rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-violet-200 bg-violet-50 px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-violet-900\">{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (aiInterim(); as interim) {\n <article class=\"ml-auto flex max-w-[72%] flex-row-reverse gap-2.5 self-end eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar--ai flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white\">\n <eds-icon name=\"graphic_eq\" [size]=\"16\" />\n </div>\n <div class=\"flex min-w-0 flex-col items-end gap-1\">\n <span class=\"font-mono text-xs font-semibold tracking-wide text-blue-600\">Ava \u00B7 AI</span>\n <div class=\"rounded-tl-xl rounded-tr rounded-br-xl rounded-bl-xl border border-blue-200 bg-blue-50 px-3.5 py-2 italic opacity-90\">\n <p class=\"m-0 text-sm leading-snug text-zinc-900\">{{ interim }}<span class=\"live-transcript-panel__caret\" aria-hidden=\"true\"></span></p>\n </div>\n </div>\n </article>\n }\n\n @if (lines().length === 0 && !patientInterim() && !aiInterim() && !staffInterim()) {\n <p class=\"m-0 px-3 py-8 text-center text-sm leading-snug text-zinc-500\">Waiting for conversation\u2026</p>\n }\n </div>\n\n @if (showStaffInput()) {\n <footer class=\"flex shrink-0 gap-2 border-t border-zinc-200 bg-white p-3\">\n <input\n type=\"text\"\n class=\"h-9 flex-1 rounded-md border border-zinc-200 bg-white px-3.5 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none\"\n placeholder=\"Type to speak as staff\u2026\"\n [ngModel]=\"staffMessage()\"\n (ngModelChange)=\"onStaffInput($event)\"\n (keyup.enter)=\"onSend()\"\n autocomplete=\"off\"\n />\n </footer>\n }\n</section>\n", styles: [".live-transcript-panel__avatar--ai{background:linear-gradient(135deg,#3b82f6,#8b5cf6)}.live-transcript-panel__caret{display:inline-block;width:7px;height:14px;margin-left:2px;vertical-align:text-bottom;background:#3b82f6;animation:eds-caret 1s step-end infinite}\n"] }]
381
+ }], ctorParameters: () => [], propDecorators: { scrollHost: [{
382
+ type: ViewChild,
383
+ args: ['scrollHost']
384
+ }] } });
385
+
386
+ const STAFF_IDENTITY_PREFIX = 'staff-';
387
+ /**
388
+ * Browser softphone for staff Listen / Take Over via LiveKit.
389
+ * Secrets stay on the server — FE only uses short-lived clientToken + livekitUrl.
390
+ *
391
+ * Root-provided on purpose: the session must survive navigating away from the
392
+ * Live Monitor page, otherwise switching dashboard tabs would drop the call.
393
+ */
394
+ class StaffMediaService {
395
+ constructor() {
396
+ this.connectionState = signal('idle');
397
+ this.mode = signal(null);
398
+ this.micEnabled = signal(false);
399
+ this.error = signal(null);
400
+ this.activeCallId = signal(null);
401
+ /** False when the browser blocks autoplay — UI should offer an "enable sound" action. */
402
+ this.audioPlaybackAllowed = signal(true);
403
+ /** True once remote caller audio is actually attached and playing. */
404
+ this.remoteAudioAttached = signal(false);
405
+ this.room = null;
406
+ this.audioContainer = null;
407
+ this.attached = new Map();
408
+ this.onTrackSubscribed = (track, publication, participant) => {
409
+ if (track.kind !== Track.Kind.Audio)
410
+ return;
411
+ // Never play another staff softphone back — that is a direct feedback loop
412
+ // where the person taking over hears their own voice.
413
+ if (this.isStaff(participant.identity)) {
414
+ try {
415
+ publication.setSubscribed(false);
416
+ }
417
+ catch {
418
+ /* server may already have unsubscribed */
419
+ }
420
+ return;
421
+ }
422
+ const container = this.ensureAudioContainer();
423
+ const element = track.attach();
424
+ element.autoplay = true;
425
+ element.setAttribute('playsinline', 'true');
426
+ element.volume = 1;
427
+ container.appendChild(element);
428
+ const key = publication.trackSid || track.sid || participant.identity;
429
+ this.attached.set(key, [...(this.attached.get(key) ?? []), element]);
430
+ this.remoteAudioAttached.set(true);
431
+ void element.play().catch(() => {
432
+ // Autoplay blocked — surfaced via the "Enable sound" action.
433
+ this.audioPlaybackAllowed.set(false);
434
+ });
435
+ };
436
+ this.onTrackUnsubscribed = (track, publication, participant) => {
437
+ if (track.kind !== Track.Kind.Audio)
438
+ return;
439
+ const key = publication.trackSid || track.sid || participant.identity;
440
+ this.detachKey(key, track);
441
+ };
442
+ this.onParticipantDisconnected = (participant) => {
443
+ for (const publication of participant.trackPublications.values()) {
444
+ const key = publication.trackSid;
445
+ if (key)
446
+ this.detachKey(key);
447
+ }
448
+ };
449
+ /** Staff tracks are dropped server-side as soon as they appear. */
450
+ this.onTrackPublished = (publication, participant) => {
451
+ if (!this.isStaff(participant.identity))
452
+ return;
453
+ try {
454
+ publication.setSubscribed(false);
455
+ }
456
+ catch {
457
+ /* nothing to unsubscribe */
458
+ }
459
+ };
460
+ }
461
+ get isConnected() {
462
+ return this.connectionState() === 'connected';
463
+ }
464
+ /** True when a softphone session is live for this call, regardless of page. */
465
+ isConnectedTo(callId) {
466
+ return this.isConnected && this.activeCallId() === callId;
467
+ }
468
+ /**
469
+ * getUserMedia only exists in a secure context (https or localhost) and, inside an
470
+ * iframe, requires allow="microphone". Returns null when capture is possible.
471
+ */
472
+ microphoneBlockedReason() {
473
+ if (typeof navigator === 'undefined')
474
+ return 'Microphone is not available in this environment.';
475
+ if (!navigator.mediaDevices?.getUserMedia) {
476
+ const secure = typeof window !== 'undefined' && window.isSecureContext;
477
+ return secure
478
+ ? 'Microphone access is blocked by the browser. If the dashboard is embedded, the host page must allow microphone access.'
479
+ : 'Microphone access needs a secure connection. Open the dashboard over https (or localhost) to talk to callers.';
480
+ }
481
+ return null;
482
+ }
483
+ async join(join) {
484
+ if (join.provider && join.provider !== 'livekit') {
485
+ throw new Error(`Unsupported media provider: ${join.provider}`);
486
+ }
487
+ if (!join.livekitUrl || !join.clientToken) {
488
+ throw new Error('Missing LiveKit URL or client token from media/join');
489
+ }
490
+ if (join.mode === 'takeover') {
491
+ const blocked = this.microphoneBlockedReason();
492
+ if (blocked)
493
+ throw new Error(blocked);
494
+ }
495
+ await this.disconnect();
496
+ this.connectionState.set('connecting');
497
+ this.error.set(null);
498
+ this.mode.set(join.mode);
499
+ this.activeCallId.set(join.callId || null);
500
+ this.micEnabled.set(false);
501
+ const room = new Room({
502
+ adaptiveStream: true,
503
+ dynacast: true,
504
+ audioCaptureDefaults: {
505
+ echoCancellation: true,
506
+ noiseSuppression: true,
507
+ autoGainControl: true,
508
+ },
509
+ });
510
+ room.on(RoomEvent.Disconnected, () => {
511
+ if (this.room !== room)
512
+ return;
513
+ void this.cleanupLocal(false);
514
+ this.connectionState.set('idle');
515
+ this.mode.set(null);
516
+ this.activeCallId.set(null);
517
+ this.micEnabled.set(false);
518
+ });
519
+ room.on(RoomEvent.ConnectionStateChanged, (state) => {
520
+ if (this.room !== room)
521
+ return;
522
+ if (state === ConnectionState.Connected) {
523
+ this.connectionState.set('connected');
524
+ }
525
+ else if (state === ConnectionState.Connecting || state === ConnectionState.Reconnecting) {
526
+ this.connectionState.set('connecting');
527
+ }
528
+ });
529
+ room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
530
+ if (this.room === room) {
531
+ this.audioPlaybackAllowed.set(room.canPlaybackAudio);
532
+ }
533
+ });
534
+ room.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed);
535
+ room.on(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed);
536
+ room.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);
537
+ room.on(RoomEvent.TrackPublished, this.onTrackPublished);
538
+ try {
539
+ this.room = room;
540
+ this.ensureAudioContainer();
541
+ await room.connect(join.livekitUrl, join.clientToken);
542
+ // Publish mic only for takeover (listen stays receive-only).
543
+ if (join.mode === 'takeover') {
544
+ await this.enableMicrophone(true);
545
+ }
546
+ this.attachExistingRemoteTracks(room);
547
+ await this.startAudioPlayback();
548
+ this.connectionState.set('connected');
549
+ }
550
+ catch (err) {
551
+ const message = this.friendlyError(err);
552
+ this.error.set(message);
553
+ this.connectionState.set('error');
554
+ await this.cleanupLocal(true);
555
+ this.mode.set(null);
556
+ this.activeCallId.set(null);
557
+ throw new Error(message);
558
+ }
559
+ }
560
+ async setMicrophoneEnabled(enabled) {
561
+ if (!this.room || this.connectionState() !== 'connected')
562
+ return;
563
+ if (this.mode() !== 'takeover' && enabled) {
564
+ // Listen mode stays muted unless upgraded to takeover.
565
+ return;
566
+ }
567
+ await this.enableMicrophone(enabled);
568
+ }
569
+ async toggleMicrophone() {
570
+ await this.setMicrophoneEnabled(!this.micEnabled());
571
+ }
572
+ /** Retry blocked autoplay — must be called from a user gesture. */
573
+ async startAudioPlayback() {
574
+ const room = this.room;
575
+ if (!room)
576
+ return;
577
+ try {
578
+ await room.startAudio();
579
+ this.audioPlaybackAllowed.set(true);
580
+ }
581
+ catch {
582
+ this.audioPlaybackAllowed.set(room.canPlaybackAudio);
583
+ }
584
+ for (const elements of this.attached.values()) {
585
+ for (const el of elements) {
586
+ void el.play().catch(() => undefined);
587
+ }
588
+ }
589
+ }
590
+ async disconnect() {
591
+ if (!this.room && this.connectionState() === 'idle')
592
+ return;
593
+ this.connectionState.set('disconnecting');
594
+ await this.cleanupLocal(true);
595
+ this.connectionState.set('idle');
596
+ this.mode.set(null);
597
+ this.activeCallId.set(null);
598
+ this.micEnabled.set(false);
599
+ this.error.set(null);
600
+ this.audioPlaybackAllowed.set(true);
601
+ }
602
+ attachExistingRemoteTracks(room) {
603
+ for (const participant of room.remoteParticipants.values()) {
604
+ for (const publication of participant.trackPublications.values()) {
605
+ if (publication.track && publication.isSubscribed) {
606
+ this.onTrackSubscribed(publication.track, publication, participant);
607
+ }
608
+ }
609
+ }
610
+ }
611
+ isStaff(identity) {
612
+ return !!identity && identity.startsWith(STAFF_IDENTITY_PREFIX);
613
+ }
614
+ detachKey(key, track) {
615
+ const elements = this.attached.get(key);
616
+ if (!elements)
617
+ return;
618
+ this.attached.delete(key);
619
+ for (const el of elements) {
620
+ try {
621
+ track?.detach(el);
622
+ }
623
+ catch {
624
+ /* already detached */
625
+ }
626
+ el.remove();
627
+ }
628
+ if (this.attached.size === 0)
629
+ this.remoteAudioAttached.set(false);
630
+ }
631
+ ensureAudioContainer() {
632
+ if (this.audioContainer?.isConnected)
633
+ return this.audioContainer;
634
+ const el = document.createElement('div');
635
+ el.dataset['edsStaffAudio'] = 'true';
636
+ el.style.position = 'fixed';
637
+ el.style.width = '0';
638
+ el.style.height = '0';
639
+ el.style.overflow = 'hidden';
640
+ document.body.appendChild(el);
641
+ this.audioContainer = el;
642
+ return el;
643
+ }
644
+ async enableMicrophone(enabled) {
645
+ const room = this.room;
646
+ if (!room)
647
+ return;
648
+ await room.localParticipant.setMicrophoneEnabled(enabled);
649
+ this.micEnabled.set(enabled);
650
+ }
651
+ friendlyError(err) {
652
+ const name = err?.name ?? '';
653
+ if (name === 'NotAllowedError' || name === 'SecurityError') {
654
+ return 'Microphone permission was denied. Allow microphone access and try again.';
655
+ }
656
+ if (name === 'NotFoundError' || name === 'OverconstrainedError') {
657
+ return 'No microphone was found on this device.';
658
+ }
659
+ if (name === 'NotReadableError' || name === 'TrackStartError') {
660
+ return 'Your microphone is in use by another app.';
661
+ }
662
+ const message = err instanceof Error ? err.message : '';
663
+ if (message.includes('getUserMedia')) {
664
+ return this.microphoneBlockedReason() ?? message;
665
+ }
666
+ return message || 'Failed to join the call audio.';
667
+ }
668
+ async cleanupLocal(disconnectRoom) {
669
+ const room = this.room;
670
+ this.room = null;
671
+ for (const key of [...this.attached.keys()]) {
672
+ this.detachKey(key);
673
+ }
674
+ this.remoteAudioAttached.set(false);
675
+ this.audioContainer?.remove();
676
+ this.audioContainer = null;
677
+ if (!room)
678
+ return;
679
+ try {
680
+ await room.localParticipant.setMicrophoneEnabled(false);
681
+ }
682
+ catch {
683
+ /* mic may never have been published */
684
+ }
685
+ try {
686
+ if (disconnectRoom) {
687
+ await room.disconnect(true);
688
+ }
689
+ room.removeAllListeners();
690
+ }
691
+ catch {
692
+ /* already gone */
693
+ }
694
+ }
695
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
696
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, providedIn: 'root' }); }
697
+ }
698
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, decorators: [{
699
+ type: Injectable,
700
+ args: [{ providedIn: 'root' }]
701
+ }] });
702
+
703
+ class LiveMonitorPageComponent {
704
+ constructor() {
705
+ this.api = inject(AiReceptionistApiService);
706
+ this.route = inject(ActivatedRoute);
707
+ this.stream = inject(LiveTranscriptService);
708
+ this.staffMedia = inject(StaffMediaService);
709
+ this.socket = inject(DashboardSocketService);
710
+ this.toast = inject(ToastService);
711
+ this.destroyRef = inject(DestroyRef);
712
+ this.call = signal(null);
713
+ this.staffMessage = signal('');
714
+ this.loading = signal(true);
715
+ this.callId = signal('');
716
+ this.summary = signal(null);
717
+ this.summaryLoading = signal(false);
718
+ this.mediaBusy = signal(false);
719
+ this.liveTools = this.stream.tools;
720
+ this.callEnded = this.stream.callEnded;
721
+ this.mediaState = this.staffMedia.connectionState;
722
+ this.mediaMode = this.staffMedia.mode;
723
+ this.micEnabled = this.staffMedia.micEnabled;
724
+ this.audioPlaybackAllowed = this.staffMedia.audioPlaybackAllowed;
725
+ this.summaryLoaded = false;
726
+ this.staffJoined = false;
727
+ this.onControlChanged = (evt) => {
728
+ if (!evt?.callId || evt.callId !== this.callId())
729
+ return;
730
+ this.call.update((c) => {
731
+ if (!c)
732
+ return c;
733
+ const handedOff = evt.aiHandedOff === true || c.aiHandedOff === true;
734
+ const controlMode = evt.controlMode ?? c.controlMode;
735
+ const next = {
736
+ ...c,
737
+ controlMode,
738
+ aiPaused: typeof evt.aiPaused === 'boolean' ? evt.aiPaused : c.aiPaused,
739
+ aiHandedOff: handedOff,
740
+ assigned: evt.assigned !== undefined ? evt.assigned : c.assigned,
741
+ };
742
+ if (controlMode === 'takeover' || handedOff) {
743
+ next.status = 'takeover';
744
+ }
745
+ else if (controlMode === 'ai' || controlMode === 'listen') {
746
+ if (c.status === 'takeover')
747
+ next.status = 'ai';
748
+ }
749
+ return next;
750
+ });
751
+ };
752
+ this.toolTrackKey = toolEventTrackKey;
753
+ effect(() => {
754
+ if (this.stream.callEnded()) {
755
+ void this.staffMedia.disconnect();
756
+ this.staffJoined = false;
757
+ this.loadCallMetadataOnce();
758
+ }
759
+ });
760
+ effect(() => {
761
+ if (this.stream.summaryRequested() && !this.summaryLoaded) {
762
+ this.loadPostCallSummary();
763
+ }
764
+ });
765
+ }
766
+ ngOnInit() {
767
+ this.socket.acquire();
768
+ this.socket.on('call_control_changed', this.onControlChanged);
769
+ this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
770
+ const id = params.get('id') ?? '';
771
+ this.callId.set(id);
772
+ // The softphone lives in a root service, so a session started earlier is still
773
+ // running when this page is re-created.
774
+ this.staffJoined = this.staffMedia.isConnectedTo(id);
775
+ this.loading.set(true);
776
+ this.summary.set(null);
777
+ this.summaryLoaded = false;
778
+ this.stream.stop();
779
+ this.loadCallMetadata();
780
+ });
781
+ }
782
+ /**
783
+ * Leaving the page must never drop the caller. The LiveKit session is owned by the
784
+ * root StaffMediaService and keeps running until End Call or an explicit leave.
785
+ */
786
+ ngOnDestroy() {
787
+ this.socket.off('call_control_changed', this.onControlChanged);
788
+ this.socket.release();
789
+ this.stream.stop();
790
+ }
791
+ /** True when this page's call is the one the softphone is connected to. */
792
+ isMediaForThisCall() {
793
+ return this.staffMedia.activeCallId() === this.callId();
794
+ }
795
+ isTakeover() {
796
+ if (this.mediaMode() === 'takeover' && this.isMediaForThisCall())
797
+ return true;
798
+ const c = this.call();
799
+ if (!c)
800
+ return false;
801
+ if (c.aiHandedOff)
802
+ return true;
803
+ if (c.controlMode === 'listen')
804
+ return false;
805
+ return c.controlMode === 'takeover' || c.status === 'takeover' || !!c.assigned;
806
+ }
807
+ /** After takeover the AI is gone for good — no resume path is offered. */
808
+ isHandedOff() {
809
+ return this.call()?.aiHandedOff === true || this.isTakeover();
810
+ }
811
+ isPaused() {
812
+ const c = this.call();
813
+ return c?.controlMode === 'paused' || !!c?.aiPaused;
814
+ }
815
+ isLive() {
816
+ return !!this.call() && !this.callEnded();
817
+ }
818
+ confidenceLevel(confidence) {
819
+ if (confidence >= 70)
820
+ return 'high';
821
+ if (confidence < 50)
822
+ return 'low';
823
+ return 'mid';
824
+ }
825
+ sentimentPercent(sentiment) {
826
+ const map = {
827
+ calm: 85,
828
+ neutral: 60,
829
+ frustrated: 35,
830
+ urgent: 15,
831
+ };
832
+ return map[sentiment];
833
+ }
834
+ toolStateClass(state) {
835
+ return `live-monitor-page__tool--${state}`;
836
+ }
837
+ isUnknownCaller(call) {
838
+ return isUnknownCaller(call.name, call.matched);
839
+ }
840
+ displayName(call) {
841
+ return this.isUnknownCaller(call) ? 'Unknown Caller' : call.name.trim();
842
+ }
843
+ profileInitials(call) {
844
+ return this.isUnknownCaller(call) ? '?' : call.initials || '?';
845
+ }
846
+ profileMeta(call) {
847
+ return callerMetaLine({
848
+ name: call.name,
849
+ matched: call.matched,
850
+ phone: call.phone,
851
+ dob: call.dob,
852
+ age: call.age ?? ageFromDob(call.dob),
853
+ gender: call.gender,
854
+ });
855
+ }
856
+ goalBadge(call) {
857
+ const raw = (call.goal || call.reason || '').trim();
858
+ if (!raw)
859
+ return null;
860
+ return raw.replace(/[_-]+/g, ' ').toUpperCase();
861
+ }
862
+ takeover() {
863
+ void this.joinMedia('takeover');
864
+ }
865
+ toggleMic() {
866
+ void this.staffMedia.toggleMicrophone();
867
+ }
868
+ transfer() {
869
+ this.api.postTransfer(this.callId()).subscribe(() => this.refreshMetadata());
870
+ }
871
+ endCall() {
872
+ const id = this.callId();
873
+ void this.staffMedia.disconnect().then(() => {
874
+ this.staffJoined = false;
875
+ this.api.postEnd(id).subscribe(() => {
876
+ this.stream.markCompleted();
877
+ this.loadCallMetadataOnce();
878
+ this.loadPostCallSummary();
879
+ });
880
+ });
881
+ }
882
+ sendMessage() {
883
+ const text = this.staffMessage().trim();
884
+ if (!text)
885
+ return;
886
+ this.api.postMessage(this.callId(), text).subscribe(() => {
887
+ this.staffMessage.set('');
888
+ this.refreshMetadata();
889
+ });
890
+ }
891
+ enableSound() {
892
+ void this.staffMedia.startAudioPlayback();
893
+ }
894
+ async joinMedia(mode) {
895
+ if (this.mediaBusy() || this.callEnded())
896
+ return;
897
+ const id = this.callId();
898
+ if (!id)
899
+ return;
900
+ // Check the mic before joining so a blocked browser never leaves the AI paused.
901
+ if (mode === 'takeover' && !this.api.usesMockStreaming()) {
902
+ const blocked = this.staffMedia.microphoneBlockedReason();
903
+ if (blocked) {
904
+ this.toast.show(blocked, 'error', 6000);
905
+ return;
906
+ }
907
+ }
908
+ this.mediaBusy.set(true);
909
+ try {
910
+ const { data } = await firstValueFrom(this.api.postMediaJoin(id, mode));
911
+ this.applyJoinMetadata(data);
912
+ if (!this.api.usesMockStreaming()) {
913
+ await this.staffMedia.join(data);
914
+ }
915
+ this.staffJoined = true;
916
+ this.toast.show(mode === 'takeover' ? 'You are on the call' : 'Listening to the call', 'success');
917
+ }
918
+ catch (err) {
919
+ const message = err instanceof Error ? err.message : `Failed to ${mode === 'takeover' ? 'take over' : 'listen'}`;
920
+ this.toast.show(message, 'error');
921
+ try {
922
+ await this.staffMedia.disconnect();
923
+ await firstValueFrom(this.api.postMediaLeave(id));
924
+ }
925
+ catch {
926
+ /* ignore cleanup errors */
927
+ }
928
+ this.staffJoined = false;
929
+ }
930
+ finally {
931
+ this.mediaBusy.set(false);
932
+ }
933
+ }
934
+ /**
935
+ * Disconnect LiveKit first, then tell the server to drop staff and restore AI.
936
+ * Only valid in listen mode — a taken-over call stays with the human.
937
+ * Does not hang up the patient.
938
+ */
939
+ async leaveMedia(notifyServer) {
940
+ const id = this.callId();
941
+ const wasJoined = this.staffJoined || this.staffMedia.isConnectedTo(id);
942
+ try {
943
+ await this.staffMedia.disconnect();
944
+ }
945
+ catch {
946
+ /* ignore */
947
+ }
948
+ this.staffJoined = false;
949
+ if (!notifyServer || !wasJoined || !id || this.callEnded())
950
+ return;
951
+ this.mediaBusy.set(true);
952
+ try {
953
+ await firstValueFrom(this.api.postResumeAi(id));
954
+ this.call.update((c) => c
955
+ ? {
956
+ ...c,
957
+ status: c.status === 'takeover' ? 'ai' : c.status,
958
+ controlMode: 'ai',
959
+ assigned: null,
960
+ aiPaused: false,
961
+ }
962
+ : c);
963
+ }
964
+ catch (err) {
965
+ const message = err instanceof Error ? err.message : 'Failed to resume AI';
966
+ this.toast.show(message, 'error');
967
+ }
968
+ finally {
969
+ this.mediaBusy.set(false);
970
+ }
971
+ }
972
+ applyJoinMetadata(data) {
973
+ this.call.update((c) => {
974
+ if (!c)
975
+ return c;
976
+ const controlMode = data.controlMode || data.mode;
977
+ return {
978
+ ...c,
979
+ controlMode,
980
+ aiPaused: typeof data.aiPaused === 'boolean' ? data.aiPaused : data.mode === 'takeover',
981
+ aiHandedOff: data.aiHandedOff === true || data.mode === 'takeover' || c.aiHandedOff === true,
982
+ assigned: data.assigned !== undefined && data.assigned !== null
983
+ ? data.assigned
984
+ : data.mode === 'takeover'
985
+ ? c.assigned || 'You'
986
+ : c.assigned,
987
+ status: data.mode === 'takeover' ? 'takeover' : c.status,
988
+ };
989
+ });
990
+ }
991
+ /** Initial load — seeds transcript/tools from REST, then starts socket. */
992
+ loadCallMetadata() {
993
+ const id = this.callId();
994
+ if (!id)
995
+ return;
996
+ this.api.getLiveCall(id).subscribe(({ data }) => {
997
+ this.call.set(data);
998
+ this.loading.set(false);
999
+ if (data) {
1000
+ this.stream.seed(data.transcript, data.tools);
1001
+ if (!this.callEnded()) {
1002
+ this.stream.watch(id, true);
1003
+ }
1004
+ }
1005
+ });
1006
+ }
1007
+ /** Metadata-only refresh after staff actions — does not overwrite live transcript/tools. */
1008
+ refreshMetadata() {
1009
+ const id = this.callId();
1010
+ if (!id)
1011
+ return;
1012
+ this.api.getLiveCall(id).subscribe(({ data }) => {
1013
+ if (data)
1014
+ this.call.set(data);
1015
+ });
1016
+ }
1017
+ /** One-time metadata refresh after call ends (no socket). */
1018
+ loadCallMetadataOnce() {
1019
+ const id = this.callId();
1020
+ if (!id)
1021
+ return;
1022
+ this.api.getLiveCall(id).subscribe(({ data }) => {
1023
+ if (data) {
1024
+ this.call.set(data);
1025
+ }
1026
+ });
1027
+ }
1028
+ loadPostCallSummary() {
1029
+ const id = this.callId();
1030
+ if (!id || this.summaryLoaded)
1031
+ return;
1032
+ this.summaryLoading.set(true);
1033
+ this.api.getCallSummary(id).subscribe(({ data }) => {
1034
+ this.summaryLoading.set(false);
1035
+ this.summaryLoaded = true;
1036
+ if (!data)
1037
+ return;
1038
+ this.summary.set(data.summary);
1039
+ this.stream.seed(data.transcript, data.tools);
1040
+ });
1041
+ }
1042
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveMonitorPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1043
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: LiveMonitorPageComponent, isStandalone: true, selector: "eds-live-monitor-page", ngImport: i0, template: "<div class=\"eds-page live-monitor-page box-border flex h-full min-h-0 flex-col overflow-hidden bg-zinc-100 p-5 text-zinc-900\">\n @if (loading()) {\n <p class=\"text-sm leading-snug text-zinc-500\">Loading call\u2026</p>\n } @else {\n @if (call(); as c) {\n <header class=\"live-monitor-page__header mb-4 flex shrink-0 flex-wrap items-center gap-3\">\n <a routerLink=\"../live\" class=\"live-monitor-page__back inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-white text-zinc-600 no-underline\" aria-label=\"Back to live calls\">\n <eds-icon name=\"arrow_back\" [size]=\"19\" />\n </a>\n\n <div class=\"live-monitor-page__profile flex min-w-56 flex-1 items-center gap-3\">\n <eds-avatar\n class=\"live-monitor-page__avatar shrink-0\"\n [initials]=\"profileInitials(c)\"\n [tone]=\"isUnknownCaller(c) ? 'gray' : 'brand'\"\n />\n\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2.5\">\n <h1 class=\"m-0 text-lg font-bold leading-snug text-zinc-900\">{{ displayName(c) }}</h1>\n @if (goalBadge(c); as badge) {\n <span class=\"inline-flex items-center gap-1 whitespace-nowrap rounded-md bg-blue-50 px-2.5 py-1 text-xs font-bold tracking-wide text-blue-700\">{{ badge }}</span>\n }\n </div>\n <p class=\"mt-1 text-xs leading-snug text-zinc-500\">{{ profileMeta(c) }}</p>\n </div>\n </div>\n\n <div class=\"live-monitor-page__header-actions ml-auto flex flex-wrap items-center gap-2\">\n @if (!callEnded()) {\n @if (mediaState() === 'connected' && isMediaForThisCall() && !audioPlaybackAllowed()) {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--ghost inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-200 bg-white px-3.5 text-sm font-semibold text-zinc-700\"\n (click)=\"enableSound()\"\n >\n <eds-icon name=\"volume_up\" [size]=\"18\" />\n Enable sound\n </button>\n }\n @if (isMediaForThisCall() && (mediaState() === 'connected' || mediaState() === 'connecting')) {\n <span\n class=\"live-monitor-page__media-pill inline-flex h-7 items-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 text-xs font-semibold tracking-wide text-zinc-600\"\n [attr.data-mode]=\"mediaMode() || 'idle'\"\n [attr.data-state]=\"mediaState()\"\n >\n @if (mediaState() === 'connecting') {\n Connecting\u2026\n } @else if (mediaMode() === 'takeover') {\n {{ micEnabled() ? 'On call \u00B7 mic on' : 'On call \u00B7 mic off' }}\n } @else {\n Joined\n }\n </span>\n }\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--danger inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-red-300 bg-white px-3.5 text-sm font-semibold text-red-600\"\n [disabled]=\"mediaBusy()\"\n (click)=\"endCall()\"\n >\n <eds-icon name=\"call_end\" [size]=\"18\" />\n End Call\n </button>\n @if (isTakeover()) {\n @if (mediaState() === 'connected' && isMediaForThisCall()) {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--ghost inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-200 bg-white px-3.5 text-sm font-semibold text-zinc-700\"\n (click)=\"toggleMic()\"\n >\n <eds-icon [name]=\"micEnabled() ? 'mic' : 'mic_off'\" [size]=\"18\" />\n {{ micEnabled() ? 'Mute' : 'Unmute' }}\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--primary inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-blue-600 bg-blue-600 px-3.5 text-sm font-semibold text-white\"\n [disabled]=\"mediaBusy()\"\n (click)=\"takeover()\"\n >\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Rejoin Audio\n </button>\n }\n } @else {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--primary inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-blue-600 bg-blue-600 px-3.5 text-sm font-semibold text-white\"\n [disabled]=\"mediaBusy()\"\n (click)=\"takeover()\"\n >\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Take Over\n </button>\n }\n } @else {\n <span class=\"inline-flex items-center gap-2 text-lg font-bold font-mono text-zinc-900\">\n <span class=\"h-2 w-2 rounded-full bg-green-500 ring-4 ring-green-500/15\" aria-hidden=\"true\"></span>\n {{ c.started | edsElapsed }}\n </span>\n }\n </div>\n </header>\n\n @if (callEnded()) {\n <div class=\"mb-4 flex items-center gap-2 rounded-lg bg-green-50 px-3.5 py-2.5 text-sm leading-snug text-green-700\" role=\"status\">\n <eds-icon name=\"check_circle\" [size]=\"18\" />\n Call ended\n </div>\n }\n\n <div class=\"live-monitor-page__grid min-h-0 flex-1\">\n <div class=\"live-monitor-page__main flex h-full min-h-0 flex-col gap-4\">\n <eds-live-transcript-panel\n [live]=\"isLive()\"\n [paused]=\"isPaused()\"\n [humanTakeover]=\"isTakeover()\"\n [showStaffInput]=\"isTakeover() && !callEnded()\"\n [staffMessage]=\"staffMessage()\"\n [callerName]=\"c.name\"\n [callerMatched]=\"c.matched\"\n (staffMessageChange)=\"staffMessage.set($event)\"\n (sendMessage)=\"sendMessage()\"\n />\n\n @if (callEnded()) {\n <section class=\"live-monitor-page__post-call flex flex-col gap-4\">\n <div class=\"rounded-lg border border-zinc-200 bg-white p-4 text-sm leading-normal text-zinc-600\">\n <h2 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Call summary</h2>\n @if (summaryLoading()) {\n <p class=\"m-0 text-zinc-500\">Generating summary\u2026</p>\n } @else if (summary()) {\n <p>{{ summary() }}</p>\n } @else {\n <p class=\"m-0 text-zinc-500\">Summary will appear when processing completes.</p>\n }\n </div>\n <eds-call-recording-player [callId]=\"callId()\" />\n </section>\n }\n </div>\n\n <aside class=\"live-monitor-page__sidebar flex h-full min-h-0 flex-col gap-3 overflow-auto\">\n @if (!callEnded()) {\n <div class=\"live-monitor-page__orb-wrap relative flex w-full min-h-40 flex-col items-center justify-center gap-3 rounded-lg border border-zinc-200 bg-white px-4 py-5\" [attr.data-mode]=\"c.status === 'transferring' ? 'transfer' : 'ai'\">\n <span class=\"live-monitor-page__ring live-monitor-page__ring--1\"></span>\n <span class=\"live-monitor-page__ring live-monitor-page__ring--2\"></span>\n <span class=\"live-monitor-page__orb relative z-[1] flex h-12 w-12 items-center justify-center rounded-full\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" />\n </span>\n <span class=\"live-monitor-page__confidence max-w-full text-center text-xs font-medium font-mono leading-snug text-zinc-500\" [attr.data-level]=\"confidenceLevel(c.confidence)\">\n AI confidence {{ c.confidence }}%\n </span>\n </div>\n\n <section class=\"rounded-lg border border-zinc-200 bg-white p-3.5\">\n <h3 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Patient sentiment</h3>\n <div class=\"mb-1.5 h-[5px] overflow-hidden rounded-sm bg-zinc-200\">\n <span\n class=\"live-monitor-page__sentiment-fill block h-full rounded-sm bg-green-500\"\n [style.width.%]=\"sentimentPercent(c.sentiment)\"\n [attr.data-sentiment]=\"c.sentiment\"\n ></span>\n </div>\n <span class=\"text-xs font-medium font-mono capitalize leading-snug text-zinc-500\">{{ c.sentiment }}</span>\n </section>\n }\n\n <section class=\"live-monitor-page__sidebar-block flex min-h-0 flex-1 flex-col rounded-lg border border-zinc-200 bg-white p-3.5\">\n <h3 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Goal Timeline</h3>\n <p class=\"mb-3 m-0 text-sm leading-snug text-zinc-600\">{{ c.goal || '\u2014' }}</p>\n <ul class=\"live-monitor-page__tools flex min-h-0 flex-1 flex-col gap-2 overflow-auto\">\n @for (tool of liveTools(); track toolTrackKey(tool, $index)) {\n <li [class]=\"toolStateClass(tool.state)\">\n <span class=\"whitespace-nowrap text-xs font-medium font-mono leading-snug text-zinc-400\">{{ tool.t }}</span>\n <div class=\"flex min-w-0 flex-col gap-0.5\">\n <span class=\"break-words text-sm font-semibold leading-snug text-zinc-900\">{{ tool.name }}</span>\n <span class=\"break-words text-xs leading-snug text-zinc-500\">{{ tool.result }}</span>\n </div>\n @if (tool.state === 'active') {\n <eds-icon name=\"sync\" [size]=\"14\" class=\"live-monitor-page__tool-spin col-span-full justify-self-end\" />\n }\n </li>\n } @empty {\n <li class=\"live-monitor-page__tools-empty text-xs leading-snug text-zinc-500\">No tool activity yet.</li>\n }\n </ul>\n </section>\n </aside>\n </div>\n } @else {\n <p class=\"text-sm leading-snug text-zinc-500\">Call ended or not found.</p>\n }\n }\n</div>\n", styles: [":host{display:block;height:calc(100vh - var(--eds-topbar-height, 0px));box-sizing:border-box}.live-monitor-page{box-sizing:border-box;color:var(--eds-text-primary, #18181b);background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__back:hover{background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__avatar ::ng-deep .eds-avatar{width:44px;height:44px;border-radius:10px;font-size:14px}.live-monitor-page__avatar ::ng-deep .eds-avatar[data-tone=brand]{background:#fef3c7;color:#92400e}.live-monitor-page__header-btn:disabled{opacity:.55;cursor:not-allowed}.live-monitor-page__header-btn--danger:hover:not(:disabled){background:#fef2f2}.live-monitor-page__header-btn--primary:hover:not(:disabled){background:#1d4ed8}.live-monitor-page__header-btn--ghost:hover:not(:disabled){background:var(--eds-bg-subtle, #f7f9fa)}.live-monitor-page__media-pill[data-mode=takeover][data-state=connected]{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}.live-monitor-page__media-pill[data-mode=listen][data-state=connected]{background:#f0fdf4;border-color:#bbf7d0;color:#15803d}.live-monitor-page__duration--footer{margin-left:auto;font-size:16px}.live-monitor-page__grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(240px,3fr);gap:var(--eds-space-4);align-items:stretch}.live-monitor-page__orb{color:#fff;background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500));animation:eds-breathe 3s ease-in-out infinite}.live-monitor-page__ring{position:absolute;width:48px;height:48px;top:calc(50% - 40px);left:50%;margin-left:-24px;border-radius:50%;border:2px solid var(--eds-brand-400);animation:eds-ring 2s ease-out infinite}.live-monitor-page__ring--2{animation-delay:1s}.live-monitor-page__orb-wrap[data-mode=transfer] .live-monitor-page__orb{background:linear-gradient(135deg,var(--eds-amber-400),var(--eds-violet-500))}.live-monitor-page__confidence[data-level=high]{color:#047857}.live-monitor-page__confidence[data-level=low]{color:#b45309}.live-monitor-page__sentiment-fill{transition:width .3s ease}.live-monitor-page__sentiment-fill[data-sentiment=frustrated],.live-monitor-page__sentiment-fill[data-sentiment=urgent]{background:var(--eds-red-500, #ef4444)}.live-monitor-page__sentiment-fill[data-sentiment=neutral]{background:var(--eds-amber-400, #f59e0b)}.live-monitor-page__tools{list-style:none;margin:0;padding:0}.live-monitor-page__tools li{display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;padding:8px 10px;border-radius:var(--eds-radius-md);background:var(--eds-surface-l1, #f4f4f5);border-left:2px solid var(--eds-border-default, #d4d4d8)}.live-monitor-page__tool-spin{animation:eds-spin 1s linear infinite;color:var(--eds-brand-500)}.live-monitor-page__tool--ok{border-left-color:var(--eds-green-500, #22c55e)}.live-monitor-page__tool--warn{border-left-color:var(--eds-amber-400, #f59e0b)}.live-monitor-page__tool--error{border-left-color:var(--eds-red-500, #ef4444)}.live-monitor-page__tool--active{border-left-color:var(--eds-brand-500)}.live-monitor-page__tools-empty{border-left:none!important;background:transparent!important;padding:4px 0!important}@media(max-width:960px){:host{height:auto;min-height:calc(100vh - var(--eds-topbar-height, 0px))}.live-monitor-page{height:auto;overflow:visible}.live-monitor-page__grid{grid-template-columns:1fr}.live-monitor-page__sidebar{width:100%;max-width:none;height:auto}.live-monitor-page__orb-wrap{width:100%}.live-monitor-page__header-actions{width:100%;margin-left:0}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: EdsAvatarComponent, selector: "eds-avatar", inputs: ["initials", "tone", "shape", "gradient"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { kind: "pipe", type: EdsElapsedPipe, name: "edsElapsed" }, { kind: "component", type: LiveTranscriptPanelComponent, selector: "eds-live-transcript-panel", inputs: ["live", "paused", "humanTakeover", "showStaffInput", "staffMessage", "callerName", "callerMatched"], outputs: ["staffMessageChange", "sendMessage"] }, { kind: "component", type: CallRecordingPlayerComponent, selector: "eds-call-recording-player", inputs: ["callId"], outputs: ["currentTimeChange", "durationChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1044
+ }
1045
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveMonitorPageComponent, decorators: [{
1046
+ type: Component,
1047
+ args: [{ selector: 'eds-live-monitor-page', imports: [
1048
+ RouterLink,
1049
+ FormsModule,
1050
+ EdsAvatarComponent,
1051
+ EdsIconComponent,
1052
+ EdsElapsedPipe,
1053
+ LiveTranscriptPanelComponent,
1054
+ CallRecordingPlayerComponent,
1055
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page live-monitor-page box-border flex h-full min-h-0 flex-col overflow-hidden bg-zinc-100 p-5 text-zinc-900\">\n @if (loading()) {\n <p class=\"text-sm leading-snug text-zinc-500\">Loading call\u2026</p>\n } @else {\n @if (call(); as c) {\n <header class=\"live-monitor-page__header mb-4 flex shrink-0 flex-wrap items-center gap-3\">\n <a routerLink=\"../live\" class=\"live-monitor-page__back inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-white text-zinc-600 no-underline\" aria-label=\"Back to live calls\">\n <eds-icon name=\"arrow_back\" [size]=\"19\" />\n </a>\n\n <div class=\"live-monitor-page__profile flex min-w-56 flex-1 items-center gap-3\">\n <eds-avatar\n class=\"live-monitor-page__avatar shrink-0\"\n [initials]=\"profileInitials(c)\"\n [tone]=\"isUnknownCaller(c) ? 'gray' : 'brand'\"\n />\n\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2.5\">\n <h1 class=\"m-0 text-lg font-bold leading-snug text-zinc-900\">{{ displayName(c) }}</h1>\n @if (goalBadge(c); as badge) {\n <span class=\"inline-flex items-center gap-1 whitespace-nowrap rounded-md bg-blue-50 px-2.5 py-1 text-xs font-bold tracking-wide text-blue-700\">{{ badge }}</span>\n }\n </div>\n <p class=\"mt-1 text-xs leading-snug text-zinc-500\">{{ profileMeta(c) }}</p>\n </div>\n </div>\n\n <div class=\"live-monitor-page__header-actions ml-auto flex flex-wrap items-center gap-2\">\n @if (!callEnded()) {\n @if (mediaState() === 'connected' && isMediaForThisCall() && !audioPlaybackAllowed()) {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--ghost inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-200 bg-white px-3.5 text-sm font-semibold text-zinc-700\"\n (click)=\"enableSound()\"\n >\n <eds-icon name=\"volume_up\" [size]=\"18\" />\n Enable sound\n </button>\n }\n @if (isMediaForThisCall() && (mediaState() === 'connected' || mediaState() === 'connecting')) {\n <span\n class=\"live-monitor-page__media-pill inline-flex h-7 items-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 text-xs font-semibold tracking-wide text-zinc-600\"\n [attr.data-mode]=\"mediaMode() || 'idle'\"\n [attr.data-state]=\"mediaState()\"\n >\n @if (mediaState() === 'connecting') {\n Connecting\u2026\n } @else if (mediaMode() === 'takeover') {\n {{ micEnabled() ? 'On call \u00B7 mic on' : 'On call \u00B7 mic off' }}\n } @else {\n Joined\n }\n </span>\n }\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--danger inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-red-300 bg-white px-3.5 text-sm font-semibold text-red-600\"\n [disabled]=\"mediaBusy()\"\n (click)=\"endCall()\"\n >\n <eds-icon name=\"call_end\" [size]=\"18\" />\n End Call\n </button>\n @if (isTakeover()) {\n @if (mediaState() === 'connected' && isMediaForThisCall()) {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--ghost inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-200 bg-white px-3.5 text-sm font-semibold text-zinc-700\"\n (click)=\"toggleMic()\"\n >\n <eds-icon [name]=\"micEnabled() ? 'mic' : 'mic_off'\" [size]=\"18\" />\n {{ micEnabled() ? 'Mute' : 'Unmute' }}\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--primary inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-blue-600 bg-blue-600 px-3.5 text-sm font-semibold text-white\"\n [disabled]=\"mediaBusy()\"\n (click)=\"takeover()\"\n >\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Rejoin Audio\n </button>\n }\n } @else {\n <button\n type=\"button\"\n class=\"live-monitor-page__header-btn live-monitor-page__header-btn--primary inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border border-blue-600 bg-blue-600 px-3.5 text-sm font-semibold text-white\"\n [disabled]=\"mediaBusy()\"\n (click)=\"takeover()\"\n >\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Take Over\n </button>\n }\n } @else {\n <span class=\"inline-flex items-center gap-2 text-lg font-bold font-mono text-zinc-900\">\n <span class=\"h-2 w-2 rounded-full bg-green-500 ring-4 ring-green-500/15\" aria-hidden=\"true\"></span>\n {{ c.started | edsElapsed }}\n </span>\n }\n </div>\n </header>\n\n @if (callEnded()) {\n <div class=\"mb-4 flex items-center gap-2 rounded-lg bg-green-50 px-3.5 py-2.5 text-sm leading-snug text-green-700\" role=\"status\">\n <eds-icon name=\"check_circle\" [size]=\"18\" />\n Call ended\n </div>\n }\n\n <div class=\"live-monitor-page__grid min-h-0 flex-1\">\n <div class=\"live-monitor-page__main flex h-full min-h-0 flex-col gap-4\">\n <eds-live-transcript-panel\n [live]=\"isLive()\"\n [paused]=\"isPaused()\"\n [humanTakeover]=\"isTakeover()\"\n [showStaffInput]=\"isTakeover() && !callEnded()\"\n [staffMessage]=\"staffMessage()\"\n [callerName]=\"c.name\"\n [callerMatched]=\"c.matched\"\n (staffMessageChange)=\"staffMessage.set($event)\"\n (sendMessage)=\"sendMessage()\"\n />\n\n @if (callEnded()) {\n <section class=\"live-monitor-page__post-call flex flex-col gap-4\">\n <div class=\"rounded-lg border border-zinc-200 bg-white p-4 text-sm leading-normal text-zinc-600\">\n <h2 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Call summary</h2>\n @if (summaryLoading()) {\n <p class=\"m-0 text-zinc-500\">Generating summary\u2026</p>\n } @else if (summary()) {\n <p>{{ summary() }}</p>\n } @else {\n <p class=\"m-0 text-zinc-500\">Summary will appear when processing completes.</p>\n }\n </div>\n <eds-call-recording-player [callId]=\"callId()\" />\n </section>\n }\n </div>\n\n <aside class=\"live-monitor-page__sidebar flex h-full min-h-0 flex-col gap-3 overflow-auto\">\n @if (!callEnded()) {\n <div class=\"live-monitor-page__orb-wrap relative flex w-full min-h-40 flex-col items-center justify-center gap-3 rounded-lg border border-zinc-200 bg-white px-4 py-5\" [attr.data-mode]=\"c.status === 'transferring' ? 'transfer' : 'ai'\">\n <span class=\"live-monitor-page__ring live-monitor-page__ring--1\"></span>\n <span class=\"live-monitor-page__ring live-monitor-page__ring--2\"></span>\n <span class=\"live-monitor-page__orb relative z-[1] flex h-12 w-12 items-center justify-center rounded-full\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" />\n </span>\n <span class=\"live-monitor-page__confidence max-w-full text-center text-xs font-medium font-mono leading-snug text-zinc-500\" [attr.data-level]=\"confidenceLevel(c.confidence)\">\n AI confidence {{ c.confidence }}%\n </span>\n </div>\n\n <section class=\"rounded-lg border border-zinc-200 bg-white p-3.5\">\n <h3 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Patient sentiment</h3>\n <div class=\"mb-1.5 h-[5px] overflow-hidden rounded-sm bg-zinc-200\">\n <span\n class=\"live-monitor-page__sentiment-fill block h-full rounded-sm bg-green-500\"\n [style.width.%]=\"sentimentPercent(c.sentiment)\"\n [attr.data-sentiment]=\"c.sentiment\"\n ></span>\n </div>\n <span class=\"text-xs font-medium font-mono capitalize leading-snug text-zinc-500\">{{ c.sentiment }}</span>\n </section>\n }\n\n <section class=\"live-monitor-page__sidebar-block flex min-h-0 flex-1 flex-col rounded-lg border border-zinc-200 bg-white p-3.5\">\n <h3 class=\"mb-2.5 m-0 text-sm font-semibold text-zinc-600\">Goal Timeline</h3>\n <p class=\"mb-3 m-0 text-sm leading-snug text-zinc-600\">{{ c.goal || '\u2014' }}</p>\n <ul class=\"live-monitor-page__tools flex min-h-0 flex-1 flex-col gap-2 overflow-auto\">\n @for (tool of liveTools(); track toolTrackKey(tool, $index)) {\n <li [class]=\"toolStateClass(tool.state)\">\n <span class=\"whitespace-nowrap text-xs font-medium font-mono leading-snug text-zinc-400\">{{ tool.t }}</span>\n <div class=\"flex min-w-0 flex-col gap-0.5\">\n <span class=\"break-words text-sm font-semibold leading-snug text-zinc-900\">{{ tool.name }}</span>\n <span class=\"break-words text-xs leading-snug text-zinc-500\">{{ tool.result }}</span>\n </div>\n @if (tool.state === 'active') {\n <eds-icon name=\"sync\" [size]=\"14\" class=\"live-monitor-page__tool-spin col-span-full justify-self-end\" />\n }\n </li>\n } @empty {\n <li class=\"live-monitor-page__tools-empty text-xs leading-snug text-zinc-500\">No tool activity yet.</li>\n }\n </ul>\n </section>\n </aside>\n </div>\n } @else {\n <p class=\"text-sm leading-snug text-zinc-500\">Call ended or not found.</p>\n }\n }\n</div>\n", styles: [":host{display:block;height:calc(100vh - var(--eds-topbar-height, 0px));box-sizing:border-box}.live-monitor-page{box-sizing:border-box;color:var(--eds-text-primary, #18181b);background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__back:hover{background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__avatar ::ng-deep .eds-avatar{width:44px;height:44px;border-radius:10px;font-size:14px}.live-monitor-page__avatar ::ng-deep .eds-avatar[data-tone=brand]{background:#fef3c7;color:#92400e}.live-monitor-page__header-btn:disabled{opacity:.55;cursor:not-allowed}.live-monitor-page__header-btn--danger:hover:not(:disabled){background:#fef2f2}.live-monitor-page__header-btn--primary:hover:not(:disabled){background:#1d4ed8}.live-monitor-page__header-btn--ghost:hover:not(:disabled){background:var(--eds-bg-subtle, #f7f9fa)}.live-monitor-page__media-pill[data-mode=takeover][data-state=connected]{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}.live-monitor-page__media-pill[data-mode=listen][data-state=connected]{background:#f0fdf4;border-color:#bbf7d0;color:#15803d}.live-monitor-page__duration--footer{margin-left:auto;font-size:16px}.live-monitor-page__grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(240px,3fr);gap:var(--eds-space-4);align-items:stretch}.live-monitor-page__orb{color:#fff;background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500));animation:eds-breathe 3s ease-in-out infinite}.live-monitor-page__ring{position:absolute;width:48px;height:48px;top:calc(50% - 40px);left:50%;margin-left:-24px;border-radius:50%;border:2px solid var(--eds-brand-400);animation:eds-ring 2s ease-out infinite}.live-monitor-page__ring--2{animation-delay:1s}.live-monitor-page__orb-wrap[data-mode=transfer] .live-monitor-page__orb{background:linear-gradient(135deg,var(--eds-amber-400),var(--eds-violet-500))}.live-monitor-page__confidence[data-level=high]{color:#047857}.live-monitor-page__confidence[data-level=low]{color:#b45309}.live-monitor-page__sentiment-fill{transition:width .3s ease}.live-monitor-page__sentiment-fill[data-sentiment=frustrated],.live-monitor-page__sentiment-fill[data-sentiment=urgent]{background:var(--eds-red-500, #ef4444)}.live-monitor-page__sentiment-fill[data-sentiment=neutral]{background:var(--eds-amber-400, #f59e0b)}.live-monitor-page__tools{list-style:none;margin:0;padding:0}.live-monitor-page__tools li{display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;padding:8px 10px;border-radius:var(--eds-radius-md);background:var(--eds-surface-l1, #f4f4f5);border-left:2px solid var(--eds-border-default, #d4d4d8)}.live-monitor-page__tool-spin{animation:eds-spin 1s linear infinite;color:var(--eds-brand-500)}.live-monitor-page__tool--ok{border-left-color:var(--eds-green-500, #22c55e)}.live-monitor-page__tool--warn{border-left-color:var(--eds-amber-400, #f59e0b)}.live-monitor-page__tool--error{border-left-color:var(--eds-red-500, #ef4444)}.live-monitor-page__tool--active{border-left-color:var(--eds-brand-500)}.live-monitor-page__tools-empty{border-left:none!important;background:transparent!important;padding:4px 0!important}@media(max-width:960px){:host{height:auto;min-height:calc(100vh - var(--eds-topbar-height, 0px))}.live-monitor-page{height:auto;overflow:visible}.live-monitor-page__grid{grid-template-columns:1fr}.live-monitor-page__sidebar{width:100%;max-width:none;height:auto}.live-monitor-page__orb-wrap{width:100%}.live-monitor-page__header-actions{width:100%;margin-left:0}}\n"] }]
1056
+ }], ctorParameters: () => [] });
1057
+
1058
+ export { LiveMonitorPageComponent };
1059
+ //# sourceMappingURL=edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs.map