@edvak99/voice-receptionist-widget 1.1.2 → 1.1.4

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 (132) hide show
  1. package/README.md +3 -0
  2. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +307 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +1 -0
  4. package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs} +1351 -579
  5. package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map +1 -0
  6. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +278 -0
  7. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +1 -0
  8. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +418 -0
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +1 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +194 -0
  11. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +1 -0
  12. package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-AN6GcSDq.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs} +5 -4
  13. package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-AN6GcSDq.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map} +1 -1
  14. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cy_Bf7AO.mjs → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs} +2 -2
  15. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cy_Bf7AO.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map} +1 -1
  16. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs +21 -0
  17. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -0
  18. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +440 -0
  19. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +1 -0
  20. package/fesm2022/{edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs} +5 -21
  21. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map +1 -0
  22. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs +38 -0
  23. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +1 -0
  24. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +27 -0
  25. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +1 -0
  26. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs +22 -0
  27. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -0
  28. package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs} +15 -5
  29. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +1 -0
  30. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +203 -0
  31. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +1 -0
  32. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +81 -0
  33. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +1 -0
  34. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +1070 -0
  35. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +1 -0
  36. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +48 -0
  37. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +1 -0
  38. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs +127 -0
  39. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs.map +1 -0
  40. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +268 -0
  41. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +1 -0
  42. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +968 -0
  43. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +1 -0
  44. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs +317 -0
  45. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +1 -0
  46. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs} +6 -4
  47. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map} +1 -1
  48. package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
  49. package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +9 -6
  50. package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +8 -8
  51. package/lib/components/knowledge-test-modal/knowledge-test-modal.component.d.ts +28 -0
  52. package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +7 -4
  53. package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +26 -0
  54. package/lib/layout/eds-module-nav.component.d.ts +0 -3
  55. package/lib/mocks/agent-activity.seed.d.ts +2 -0
  56. package/lib/models/agent-activity.types.d.ts +54 -0
  57. package/lib/models/ai-receptionist.types.d.ts +26 -0
  58. package/lib/models/api-dto.types.d.ts +18 -0
  59. package/lib/models/live-transcript.types.d.ts +2 -1
  60. package/lib/models/outbound-patient.types.d.ts +23 -0
  61. package/lib/models/outbound-purpose.types.d.ts +2 -0
  62. package/lib/models/pagination.d.ts +11 -0
  63. package/lib/models/staff-media.types.d.ts +36 -0
  64. package/lib/pages/activities-page/activities-page.component.d.ts +57 -0
  65. package/lib/pages/analytics-page/analytics-page.component.d.ts +38 -14
  66. package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +2 -3
  67. package/lib/pages/call-history-page/call-history-page.component.d.ts +16 -9
  68. package/lib/pages/dashboard-page/dashboard-page.component.d.ts +12 -8
  69. package/lib/pages/live-calls-page/live-calls-page.component.d.ts +14 -24
  70. package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +34 -1
  71. package/lib/pages/settings-page/settings-fields.config.d.ts +16 -0
  72. package/lib/pages/settings-page/settings-page.component.d.ts +48 -14
  73. package/lib/pages/tasks-page/tasks-page.component.d.ts +6 -0
  74. package/lib/services/ai-receptionist-api.mock.d.ts +44 -9
  75. package/lib/services/ai-receptionist-api.service.d.ts +65 -10
  76. package/lib/services/live-transcript.service.d.ts +1 -0
  77. package/lib/services/staff-media.service.d.ts +52 -0
  78. package/lib/ui/eds-avatar.component.d.ts +5 -2
  79. package/lib/ui/eds-button.component.d.ts +1 -1
  80. package/lib/utils/call-history-display.util.d.ts +21 -0
  81. package/lib/utils/outbound-purpose.util.d.ts +19 -0
  82. package/lib/utils/transcript-timing.util.d.ts +1 -1
  83. package/lib/utils/welcome-message.util.d.ts +1 -1
  84. package/package.json +2 -1
  85. package/styles/_index.scss +5 -40
  86. package/styles/chat-ui.scss +18 -18
  87. package/styles/eds.scss +188 -119
  88. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs +0 -113
  89. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs.map +0 -1
  90. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs +0 -35
  91. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs.map +0 -1
  92. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs +0 -316
  93. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs.map +0 -1
  94. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs +0 -138
  95. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs.map +0 -1
  96. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +0 -70
  97. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +0 -1
  98. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs +0 -248
  99. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs.map +0 -1
  100. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +0 -27
  101. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +0 -1
  102. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs.map +0 -1
  103. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs.map +0 -1
  104. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs +0 -575
  105. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs.map +0 -1
  106. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs.map +0 -1
  107. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs +0 -115
  108. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs.map +0 -1
  109. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs +0 -574
  110. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs.map +0 -1
  111. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +0 -54
  112. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +0 -1
  113. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs +0 -265
  114. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs.map +0 -1
  115. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs +0 -509
  116. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs.map +0 -1
  117. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +0 -52
  118. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +0 -1
  119. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs +0 -237
  120. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs.map +0 -1
  121. package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +0 -22
  122. package/lib/knowledge/components/faq-form/faq-form.component.d.ts +0 -27
  123. package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +0 -17
  124. package/lib/knowledge/mocks/practice-faq.seed.d.ts +0 -2
  125. package/lib/knowledge/models/practice-faq.model.d.ts +0 -47
  126. package/lib/knowledge/pages/knowledge-hub.page.d.ts +0 -47
  127. package/lib/knowledge/services/practice-faq-api.mock.d.ts +0 -16
  128. package/lib/knowledge/services/practice-faq-api.service.d.ts +0 -22
  129. package/lib/knowledge/utils/faq.util.d.ts +0 -3
  130. package/lib/layout/eds-topbar.component.d.ts +0 -29
  131. package/lib/pages/audit-page/audit-page.component.d.ts +0 -14
  132. package/lib/ui/eds-waveform.component.d.ts +0 -8
@@ -0,0 +1,1070 @@
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 { V as VOICE_RECEPTIONIST_CONFIG, E as EdsIconComponent, T as ToastService } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.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-48cifVPF.mjs';
10
+ import { D as DashboardSocketService } from './edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs';
11
+ import { C as CallRecordingPlayerComponent } from './edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.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-CWRiJGfv.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
+ systemClass(flag) {
365
+ if (flag === 'verify')
366
+ return 'live-transcript-panel__system--verify';
367
+ if (flag === 'fail' || flag === 'escalate')
368
+ return 'live-transcript-panel__system--warn';
369
+ return 'live-transcript-panel__system--info';
370
+ }
371
+ msgClass(who) {
372
+ return `live-transcript-panel__msg--${who}`;
373
+ }
374
+ avatarClass(who) {
375
+ return `live-transcript-panel__avatar--${who}`;
376
+ }
377
+ onStaffInput(value) {
378
+ this.staffMessageChange.emit(value);
379
+ }
380
+ onSend() {
381
+ this.sendMessage.emit();
382
+ }
383
+ requestScroll() {
384
+ this.shouldScroll = true;
385
+ }
386
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
387
+ 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" }, viewQueries: [{ propertyName: "scrollHost", first: true, predicate: ["scrollHost"], descendants: true }], ngImport: i0, template: "<section class=\"live-transcript-panel\" aria-labelledby=\"live-transcript-title\">\n <header class=\"live-transcript-panel__header\">\n <h2 id=\"live-transcript-title\" class=\"live-transcript-panel__title\">Live transcript</h2>\n </header>\n\n <div #scrollHost class=\"live-transcript-panel__scroll\" role=\"log\" aria-live=\"polite\">\n @for (line of lines(); track $index) {\n @if (line.who === 'system') {\n <div class=\"live-transcript-panel__system eds-animate-fade-up\" [class]=\"systemClass(line.flag)\">\n <eds-icon [name]=\"systemIcon(line.flag)\" [size]=\"14\" />\n <span>{{ line.text }}</span>\n </div>\n } @else {\n <article class=\"live-transcript-panel__msg eds-animate-fade-up\" [class]=\"msgClass(line.who)\">\n <div class=\"live-transcript-panel__avatar\" [class]=\"avatarClass(line.who)\">\n <eds-icon [name]=\"speakerIcon(line.who)\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">{{ roleLabel(line.who) }}</span>\n <div class=\"live-transcript-panel__bubble\">\n <p>{{ line.text }}</p>\n </div>\n </div>\n </article>\n }\n }\n\n @if (patientInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--patient eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--patient\">\n <eds-icon name=\"person\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">{{ callerLabel() }} \u00B7 listening</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (staffInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--staff eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--staff\">\n <eds-icon name=\"support_agent\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">Staff \u00B7 speaking</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (aiInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--ai eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--ai\">\n <eds-icon name=\"graphic_eq\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">Ava \u00B7 AI</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ 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=\"live-transcript-panel__empty\">Waiting for conversation\u2026</p>\n }\n </div>\n\n @if (showStaffInput()) {\n <footer class=\"live-transcript-panel__footer\">\n <input\n type=\"text\"\n class=\"live-transcript-panel__input\"\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: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%}.live-transcript-panel{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);border-radius:var(--eds-radius-lg)}.live-transcript-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--eds-border-default, #e4e4e7);flex-shrink:0}.live-transcript-panel__title{margin:0;font:600 11px/1 var(--eds-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--eds-text-secondary, #71717a)}.live-transcript-panel__rec{display:flex;align-items:center;gap:10px}.live-transcript-panel__rec-label{font:600 11px/1 var(--eds-font-mono);letter-spacing:.06em;color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__scroll{flex:1;overflow:auto;padding:18px;display:flex;flex-direction:column;gap:14px;background:var(--eds-surface-l1, #fafafa)}.live-transcript-panel__empty{margin:0;text-align:center;padding:32px 12px;font:400 13px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-transcript-panel__system{align-self:center;display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:var(--eds-radius-pill);border:1px solid var(--eds-border-default, #e4e4e7);font:500 12px/1 var(--eds-font-mono);max-width:90%;text-align:center}.live-transcript-panel__system--verify{color:#047857;background:#ecfdf5;border-color:#a7f3d0}.live-transcript-panel__system--warn{color:#b45309;background:#fffbeb;border-color:#fde68a}.live-transcript-panel__system--info{color:var(--eds-text-secondary, #52525b);background:var(--eds-surface-l0, #fff);border-color:var(--eds-border-default, #e4e4e7)}.live-transcript-panel__msg{display:flex;gap:10px;max-width:72%}.live-transcript-panel__msg--patient{align-self:flex-start;margin-right:auto}.live-transcript-panel__msg--patient .live-transcript-panel__content{align-items:flex-start}.live-transcript-panel__msg--patient .live-transcript-panel__bubble{border-radius:4px 12px 12px}.live-transcript-panel__msg--ai,.live-transcript-panel__msg--staff{align-self:flex-end;margin-left:auto;flex-direction:row-reverse}.live-transcript-panel__msg--ai .live-transcript-panel__content,.live-transcript-panel__msg--staff .live-transcript-panel__content{align-items:flex-end}.live-transcript-panel__msg--ai .live-transcript-panel__bubble,.live-transcript-panel__msg--staff .live-transcript-panel__bubble{border-radius:12px 4px 12px 12px}.live-transcript-panel__msg--typing .live-transcript-panel__bubble{padding:12px 16px}.live-transcript-panel__avatar{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}.live-transcript-panel__avatar--ai{background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500))}.live-transcript-panel__avatar--staff{background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe}.live-transcript-panel__avatar--patient{background:#e4e4e7;color:#52525b}.live-transcript-panel__content{display:flex;flex-direction:column;gap:4px;min-width:0}.live-transcript-panel__label{font:600 11px/1 var(--eds-font-mono);letter-spacing:.04em;color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__msg--ai .live-transcript-panel__label{color:var(--eds-brand-600, #2563eb)}.live-transcript-panel__msg--staff .live-transcript-panel__label{color:#6d28d9}.live-transcript-panel__bubble{padding:9px 13px;border-radius:4px 12px 12px;background:#eff6ff;border:1px solid #bfdbfe}.live-transcript-panel__bubble p{margin:0;font:400 13px/19px var(--eds-font-sans);color:var(--eds-text-primary, #18181b)}.live-transcript-panel__bubble--interim{opacity:.85;font-style:italic}.live-transcript-panel__msg--patient .live-transcript-panel__bubble{background:var(--eds-surface-l0, #fff);border-color:var(--eds-border-default, #e4e4e7)}.live-transcript-panel__msg--staff .live-transcript-panel__bubble{background:#f5f3ff;border-color:#ddd6fe}.live-transcript-panel__msg--staff .live-transcript-panel__bubble p{color:#4c1d95}.live-transcript-panel__caret{display:inline-block;width:7px;height:14px;margin-left:2px;vertical-align:text-bottom;background:var(--eds-brand-500, #3b82f6);animation:eds-caret 1s step-end infinite}.live-transcript-panel__dots{display:inline-flex;align-items:center;gap:5px;height:14px}.live-transcript-panel__dots span{width:6px;height:6px;border-radius:50%;background:var(--eds-brand-500, #3b82f6);animation:eds-blink 1.4s ease-in-out infinite}.live-transcript-panel__dots span:nth-child(2){animation-delay:.2s}.live-transcript-panel__dots span:nth-child(3){animation-delay:.4s}.live-transcript-panel__footer{display:flex;gap:8px;padding:12px;border-top:1px solid var(--eds-border-default, #e4e4e7);flex-shrink:0;background:var(--eds-surface-l0, #fff)}.live-transcript-panel__input{flex:1;height:38px;padding:0 14px;border-radius:var(--eds-radius-md);border:1px solid var(--eds-border-default, #e4e4e7);background:var(--eds-surface-l0, #fff);color:var(--eds-text-primary, #18181b);font:400 13px/1 var(--eds-font-sans)}.live-transcript-panel__input::placeholder{color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__input:focus{outline:none;border-color:var(--eds-brand-500, #3b82f6)}@media(max-width:900px){:host,.live-transcript-panel{height:auto;min-height:360px;max-height:none}}\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 }); }
388
+ }
389
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveTranscriptPanelComponent, decorators: [{
390
+ type: Component,
391
+ args: [{ selector: 'eds-live-transcript-panel', imports: [FormsModule, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"live-transcript-panel\" aria-labelledby=\"live-transcript-title\">\n <header class=\"live-transcript-panel__header\">\n <h2 id=\"live-transcript-title\" class=\"live-transcript-panel__title\">Live transcript</h2>\n </header>\n\n <div #scrollHost class=\"live-transcript-panel__scroll\" role=\"log\" aria-live=\"polite\">\n @for (line of lines(); track $index) {\n @if (line.who === 'system') {\n <div class=\"live-transcript-panel__system eds-animate-fade-up\" [class]=\"systemClass(line.flag)\">\n <eds-icon [name]=\"systemIcon(line.flag)\" [size]=\"14\" />\n <span>{{ line.text }}</span>\n </div>\n } @else {\n <article class=\"live-transcript-panel__msg eds-animate-fade-up\" [class]=\"msgClass(line.who)\">\n <div class=\"live-transcript-panel__avatar\" [class]=\"avatarClass(line.who)\">\n <eds-icon [name]=\"speakerIcon(line.who)\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">{{ roleLabel(line.who) }}</span>\n <div class=\"live-transcript-panel__bubble\">\n <p>{{ line.text }}</p>\n </div>\n </div>\n </article>\n }\n }\n\n @if (patientInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--patient eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--patient\">\n <eds-icon name=\"person\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">{{ callerLabel() }} \u00B7 listening</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (staffInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--staff eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--staff\">\n <eds-icon name=\"support_agent\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">Staff \u00B7 speaking</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ interim }}</p>\n </div>\n </div>\n </article>\n }\n\n @if (aiInterim(); as interim) {\n <article class=\"live-transcript-panel__msg live-transcript-panel__msg--ai eds-animate-fade-up\">\n <div class=\"live-transcript-panel__avatar live-transcript-panel__avatar--ai\">\n <eds-icon name=\"graphic_eq\" [size]=\"16\" />\n </div>\n <div class=\"live-transcript-panel__content\">\n <span class=\"live-transcript-panel__label\">Ava \u00B7 AI</span>\n <div class=\"live-transcript-panel__bubble live-transcript-panel__bubble--interim\">\n <p>{{ 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=\"live-transcript-panel__empty\">Waiting for conversation\u2026</p>\n }\n </div>\n\n @if (showStaffInput()) {\n <footer class=\"live-transcript-panel__footer\">\n <input\n type=\"text\"\n class=\"live-transcript-panel__input\"\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: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%}.live-transcript-panel{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);border-radius:var(--eds-radius-lg)}.live-transcript-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--eds-border-default, #e4e4e7);flex-shrink:0}.live-transcript-panel__title{margin:0;font:600 11px/1 var(--eds-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--eds-text-secondary, #71717a)}.live-transcript-panel__rec{display:flex;align-items:center;gap:10px}.live-transcript-panel__rec-label{font:600 11px/1 var(--eds-font-mono);letter-spacing:.06em;color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__scroll{flex:1;overflow:auto;padding:18px;display:flex;flex-direction:column;gap:14px;background:var(--eds-surface-l1, #fafafa)}.live-transcript-panel__empty{margin:0;text-align:center;padding:32px 12px;font:400 13px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-transcript-panel__system{align-self:center;display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:var(--eds-radius-pill);border:1px solid var(--eds-border-default, #e4e4e7);font:500 12px/1 var(--eds-font-mono);max-width:90%;text-align:center}.live-transcript-panel__system--verify{color:#047857;background:#ecfdf5;border-color:#a7f3d0}.live-transcript-panel__system--warn{color:#b45309;background:#fffbeb;border-color:#fde68a}.live-transcript-panel__system--info{color:var(--eds-text-secondary, #52525b);background:var(--eds-surface-l0, #fff);border-color:var(--eds-border-default, #e4e4e7)}.live-transcript-panel__msg{display:flex;gap:10px;max-width:72%}.live-transcript-panel__msg--patient{align-self:flex-start;margin-right:auto}.live-transcript-panel__msg--patient .live-transcript-panel__content{align-items:flex-start}.live-transcript-panel__msg--patient .live-transcript-panel__bubble{border-radius:4px 12px 12px}.live-transcript-panel__msg--ai,.live-transcript-panel__msg--staff{align-self:flex-end;margin-left:auto;flex-direction:row-reverse}.live-transcript-panel__msg--ai .live-transcript-panel__content,.live-transcript-panel__msg--staff .live-transcript-panel__content{align-items:flex-end}.live-transcript-panel__msg--ai .live-transcript-panel__bubble,.live-transcript-panel__msg--staff .live-transcript-panel__bubble{border-radius:12px 4px 12px 12px}.live-transcript-panel__msg--typing .live-transcript-panel__bubble{padding:12px 16px}.live-transcript-panel__avatar{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}.live-transcript-panel__avatar--ai{background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500))}.live-transcript-panel__avatar--staff{background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe}.live-transcript-panel__avatar--patient{background:#e4e4e7;color:#52525b}.live-transcript-panel__content{display:flex;flex-direction:column;gap:4px;min-width:0}.live-transcript-panel__label{font:600 11px/1 var(--eds-font-mono);letter-spacing:.04em;color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__msg--ai .live-transcript-panel__label{color:var(--eds-brand-600, #2563eb)}.live-transcript-panel__msg--staff .live-transcript-panel__label{color:#6d28d9}.live-transcript-panel__bubble{padding:9px 13px;border-radius:4px 12px 12px;background:#eff6ff;border:1px solid #bfdbfe}.live-transcript-panel__bubble p{margin:0;font:400 13px/19px var(--eds-font-sans);color:var(--eds-text-primary, #18181b)}.live-transcript-panel__bubble--interim{opacity:.85;font-style:italic}.live-transcript-panel__msg--patient .live-transcript-panel__bubble{background:var(--eds-surface-l0, #fff);border-color:var(--eds-border-default, #e4e4e7)}.live-transcript-panel__msg--staff .live-transcript-panel__bubble{background:#f5f3ff;border-color:#ddd6fe}.live-transcript-panel__msg--staff .live-transcript-panel__bubble p{color:#4c1d95}.live-transcript-panel__caret{display:inline-block;width:7px;height:14px;margin-left:2px;vertical-align:text-bottom;background:var(--eds-brand-500, #3b82f6);animation:eds-caret 1s step-end infinite}.live-transcript-panel__dots{display:inline-flex;align-items:center;gap:5px;height:14px}.live-transcript-panel__dots span{width:6px;height:6px;border-radius:50%;background:var(--eds-brand-500, #3b82f6);animation:eds-blink 1.4s ease-in-out infinite}.live-transcript-panel__dots span:nth-child(2){animation-delay:.2s}.live-transcript-panel__dots span:nth-child(3){animation-delay:.4s}.live-transcript-panel__footer{display:flex;gap:8px;padding:12px;border-top:1px solid var(--eds-border-default, #e4e4e7);flex-shrink:0;background:var(--eds-surface-l0, #fff)}.live-transcript-panel__input{flex:1;height:38px;padding:0 14px;border-radius:var(--eds-radius-md);border:1px solid var(--eds-border-default, #e4e4e7);background:var(--eds-surface-l0, #fff);color:var(--eds-text-primary, #18181b);font:400 13px/1 var(--eds-font-sans)}.live-transcript-panel__input::placeholder{color:var(--eds-text-secondary, #a1a1aa)}.live-transcript-panel__input:focus{outline:none;border-color:var(--eds-brand-500, #3b82f6)}@media(max-width:900px){:host,.live-transcript-panel{height:auto;min-height:360px;max-height:none}}\n"] }]
392
+ }], ctorParameters: () => [], propDecorators: { scrollHost: [{
393
+ type: ViewChild,
394
+ args: ['scrollHost']
395
+ }] } });
396
+
397
+ const STAFF_IDENTITY_PREFIX = 'staff-';
398
+ /**
399
+ * Browser softphone for staff Listen / Take Over via LiveKit.
400
+ * Secrets stay on the server — FE only uses short-lived clientToken + livekitUrl.
401
+ *
402
+ * Root-provided on purpose: the session must survive navigating away from the
403
+ * Live Monitor page, otherwise switching dashboard tabs would drop the call.
404
+ */
405
+ class StaffMediaService {
406
+ constructor() {
407
+ this.connectionState = signal('idle');
408
+ this.mode = signal(null);
409
+ this.micEnabled = signal(false);
410
+ this.error = signal(null);
411
+ this.activeCallId = signal(null);
412
+ /** False when the browser blocks autoplay — UI should offer an "enable sound" action. */
413
+ this.audioPlaybackAllowed = signal(true);
414
+ /** True once remote caller audio is actually attached and playing. */
415
+ this.remoteAudioAttached = signal(false);
416
+ this.room = null;
417
+ this.audioContainer = null;
418
+ this.attached = new Map();
419
+ this.onTrackSubscribed = (track, publication, participant) => {
420
+ if (track.kind !== Track.Kind.Audio)
421
+ return;
422
+ // Never play another staff softphone back — that is a direct feedback loop
423
+ // where the person taking over hears their own voice.
424
+ if (this.isStaff(participant.identity)) {
425
+ try {
426
+ publication.setSubscribed(false);
427
+ }
428
+ catch {
429
+ /* server may already have unsubscribed */
430
+ }
431
+ return;
432
+ }
433
+ const container = this.ensureAudioContainer();
434
+ const element = track.attach();
435
+ element.autoplay = true;
436
+ element.setAttribute('playsinline', 'true');
437
+ element.volume = 1;
438
+ container.appendChild(element);
439
+ const key = publication.trackSid || track.sid || participant.identity;
440
+ this.attached.set(key, [...(this.attached.get(key) ?? []), element]);
441
+ this.remoteAudioAttached.set(true);
442
+ void element.play().catch(() => {
443
+ // Autoplay blocked — surfaced via the "Enable sound" action.
444
+ this.audioPlaybackAllowed.set(false);
445
+ });
446
+ };
447
+ this.onTrackUnsubscribed = (track, publication, participant) => {
448
+ if (track.kind !== Track.Kind.Audio)
449
+ return;
450
+ const key = publication.trackSid || track.sid || participant.identity;
451
+ this.detachKey(key, track);
452
+ };
453
+ this.onParticipantDisconnected = (participant) => {
454
+ for (const publication of participant.trackPublications.values()) {
455
+ const key = publication.trackSid;
456
+ if (key)
457
+ this.detachKey(key);
458
+ }
459
+ };
460
+ /** Staff tracks are dropped server-side as soon as they appear. */
461
+ this.onTrackPublished = (publication, participant) => {
462
+ if (!this.isStaff(participant.identity))
463
+ return;
464
+ try {
465
+ publication.setSubscribed(false);
466
+ }
467
+ catch {
468
+ /* nothing to unsubscribe */
469
+ }
470
+ };
471
+ }
472
+ get isConnected() {
473
+ return this.connectionState() === 'connected';
474
+ }
475
+ /** True when a softphone session is live for this call, regardless of page. */
476
+ isConnectedTo(callId) {
477
+ return this.isConnected && this.activeCallId() === callId;
478
+ }
479
+ /**
480
+ * getUserMedia only exists in a secure context (https or localhost) and, inside an
481
+ * iframe, requires allow="microphone". Returns null when capture is possible.
482
+ */
483
+ microphoneBlockedReason() {
484
+ if (typeof navigator === 'undefined')
485
+ return 'Microphone is not available in this environment.';
486
+ if (!navigator.mediaDevices?.getUserMedia) {
487
+ const secure = typeof window !== 'undefined' && window.isSecureContext;
488
+ return secure
489
+ ? 'Microphone access is blocked by the browser. If the dashboard is embedded, the host page must allow microphone access.'
490
+ : 'Microphone access needs a secure connection. Open the dashboard over https (or localhost) to talk to callers.';
491
+ }
492
+ return null;
493
+ }
494
+ async join(join) {
495
+ if (join.provider && join.provider !== 'livekit') {
496
+ throw new Error(`Unsupported media provider: ${join.provider}`);
497
+ }
498
+ if (!join.livekitUrl || !join.clientToken) {
499
+ throw new Error('Missing LiveKit URL or client token from media/join');
500
+ }
501
+ if (join.mode === 'takeover') {
502
+ const blocked = this.microphoneBlockedReason();
503
+ if (blocked)
504
+ throw new Error(blocked);
505
+ }
506
+ await this.disconnect();
507
+ this.connectionState.set('connecting');
508
+ this.error.set(null);
509
+ this.mode.set(join.mode);
510
+ this.activeCallId.set(join.callId || null);
511
+ this.micEnabled.set(false);
512
+ const room = new Room({
513
+ adaptiveStream: true,
514
+ dynacast: true,
515
+ audioCaptureDefaults: {
516
+ echoCancellation: true,
517
+ noiseSuppression: true,
518
+ autoGainControl: true,
519
+ },
520
+ });
521
+ room.on(RoomEvent.Disconnected, () => {
522
+ if (this.room !== room)
523
+ return;
524
+ void this.cleanupLocal(false);
525
+ this.connectionState.set('idle');
526
+ this.mode.set(null);
527
+ this.activeCallId.set(null);
528
+ this.micEnabled.set(false);
529
+ });
530
+ room.on(RoomEvent.ConnectionStateChanged, (state) => {
531
+ if (this.room !== room)
532
+ return;
533
+ if (state === ConnectionState.Connected) {
534
+ this.connectionState.set('connected');
535
+ }
536
+ else if (state === ConnectionState.Connecting || state === ConnectionState.Reconnecting) {
537
+ this.connectionState.set('connecting');
538
+ }
539
+ });
540
+ room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
541
+ if (this.room === room) {
542
+ this.audioPlaybackAllowed.set(room.canPlaybackAudio);
543
+ }
544
+ });
545
+ room.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed);
546
+ room.on(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed);
547
+ room.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);
548
+ room.on(RoomEvent.TrackPublished, this.onTrackPublished);
549
+ try {
550
+ this.room = room;
551
+ this.ensureAudioContainer();
552
+ await room.connect(join.livekitUrl, join.clientToken);
553
+ // Publish mic only for takeover (listen stays receive-only).
554
+ if (join.mode === 'takeover') {
555
+ await this.enableMicrophone(true);
556
+ }
557
+ this.attachExistingRemoteTracks(room);
558
+ await this.startAudioPlayback();
559
+ this.connectionState.set('connected');
560
+ }
561
+ catch (err) {
562
+ const message = this.friendlyError(err);
563
+ this.error.set(message);
564
+ this.connectionState.set('error');
565
+ await this.cleanupLocal(true);
566
+ this.mode.set(null);
567
+ this.activeCallId.set(null);
568
+ throw new Error(message);
569
+ }
570
+ }
571
+ async setMicrophoneEnabled(enabled) {
572
+ if (!this.room || this.connectionState() !== 'connected')
573
+ return;
574
+ if (this.mode() !== 'takeover' && enabled) {
575
+ // Listen mode stays muted unless upgraded to takeover.
576
+ return;
577
+ }
578
+ await this.enableMicrophone(enabled);
579
+ }
580
+ async toggleMicrophone() {
581
+ await this.setMicrophoneEnabled(!this.micEnabled());
582
+ }
583
+ /** Retry blocked autoplay — must be called from a user gesture. */
584
+ async startAudioPlayback() {
585
+ const room = this.room;
586
+ if (!room)
587
+ return;
588
+ try {
589
+ await room.startAudio();
590
+ this.audioPlaybackAllowed.set(true);
591
+ }
592
+ catch {
593
+ this.audioPlaybackAllowed.set(room.canPlaybackAudio);
594
+ }
595
+ for (const elements of this.attached.values()) {
596
+ for (const el of elements) {
597
+ void el.play().catch(() => undefined);
598
+ }
599
+ }
600
+ }
601
+ async disconnect() {
602
+ if (!this.room && this.connectionState() === 'idle')
603
+ return;
604
+ this.connectionState.set('disconnecting');
605
+ await this.cleanupLocal(true);
606
+ this.connectionState.set('idle');
607
+ this.mode.set(null);
608
+ this.activeCallId.set(null);
609
+ this.micEnabled.set(false);
610
+ this.error.set(null);
611
+ this.audioPlaybackAllowed.set(true);
612
+ }
613
+ attachExistingRemoteTracks(room) {
614
+ for (const participant of room.remoteParticipants.values()) {
615
+ for (const publication of participant.trackPublications.values()) {
616
+ if (publication.track && publication.isSubscribed) {
617
+ this.onTrackSubscribed(publication.track, publication, participant);
618
+ }
619
+ }
620
+ }
621
+ }
622
+ isStaff(identity) {
623
+ return !!identity && identity.startsWith(STAFF_IDENTITY_PREFIX);
624
+ }
625
+ detachKey(key, track) {
626
+ const elements = this.attached.get(key);
627
+ if (!elements)
628
+ return;
629
+ this.attached.delete(key);
630
+ for (const el of elements) {
631
+ try {
632
+ track?.detach(el);
633
+ }
634
+ catch {
635
+ /* already detached */
636
+ }
637
+ el.remove();
638
+ }
639
+ if (this.attached.size === 0)
640
+ this.remoteAudioAttached.set(false);
641
+ }
642
+ ensureAudioContainer() {
643
+ if (this.audioContainer?.isConnected)
644
+ return this.audioContainer;
645
+ const el = document.createElement('div');
646
+ el.dataset['edsStaffAudio'] = 'true';
647
+ el.style.position = 'fixed';
648
+ el.style.width = '0';
649
+ el.style.height = '0';
650
+ el.style.overflow = 'hidden';
651
+ document.body.appendChild(el);
652
+ this.audioContainer = el;
653
+ return el;
654
+ }
655
+ async enableMicrophone(enabled) {
656
+ const room = this.room;
657
+ if (!room)
658
+ return;
659
+ await room.localParticipant.setMicrophoneEnabled(enabled);
660
+ this.micEnabled.set(enabled);
661
+ }
662
+ friendlyError(err) {
663
+ const name = err?.name ?? '';
664
+ if (name === 'NotAllowedError' || name === 'SecurityError') {
665
+ return 'Microphone permission was denied. Allow microphone access and try again.';
666
+ }
667
+ if (name === 'NotFoundError' || name === 'OverconstrainedError') {
668
+ return 'No microphone was found on this device.';
669
+ }
670
+ if (name === 'NotReadableError' || name === 'TrackStartError') {
671
+ return 'Your microphone is in use by another app.';
672
+ }
673
+ const message = err instanceof Error ? err.message : '';
674
+ if (message.includes('getUserMedia')) {
675
+ return this.microphoneBlockedReason() ?? message;
676
+ }
677
+ return message || 'Failed to join the call audio.';
678
+ }
679
+ async cleanupLocal(disconnectRoom) {
680
+ const room = this.room;
681
+ this.room = null;
682
+ for (const key of [...this.attached.keys()]) {
683
+ this.detachKey(key);
684
+ }
685
+ this.remoteAudioAttached.set(false);
686
+ this.audioContainer?.remove();
687
+ this.audioContainer = null;
688
+ if (!room)
689
+ return;
690
+ try {
691
+ await room.localParticipant.setMicrophoneEnabled(false);
692
+ }
693
+ catch {
694
+ /* mic may never have been published */
695
+ }
696
+ try {
697
+ if (disconnectRoom) {
698
+ await room.disconnect(true);
699
+ }
700
+ room.removeAllListeners();
701
+ }
702
+ catch {
703
+ /* already gone */
704
+ }
705
+ }
706
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
707
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, providedIn: 'root' }); }
708
+ }
709
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: StaffMediaService, decorators: [{
710
+ type: Injectable,
711
+ args: [{ providedIn: 'root' }]
712
+ }] });
713
+
714
+ class LiveMonitorPageComponent {
715
+ constructor() {
716
+ this.api = inject(AiReceptionistApiService);
717
+ this.route = inject(ActivatedRoute);
718
+ this.stream = inject(LiveTranscriptService);
719
+ this.staffMedia = inject(StaffMediaService);
720
+ this.socket = inject(DashboardSocketService);
721
+ this.toast = inject(ToastService);
722
+ this.destroyRef = inject(DestroyRef);
723
+ this.call = signal(null);
724
+ this.staffMessage = signal('');
725
+ this.loading = signal(true);
726
+ this.callId = signal('');
727
+ this.summary = signal(null);
728
+ this.summaryLoading = signal(false);
729
+ this.mediaBusy = signal(false);
730
+ this.liveTools = this.stream.tools;
731
+ this.callEnded = this.stream.callEnded;
732
+ this.mediaState = this.staffMedia.connectionState;
733
+ this.mediaMode = this.staffMedia.mode;
734
+ this.micEnabled = this.staffMedia.micEnabled;
735
+ this.audioPlaybackAllowed = this.staffMedia.audioPlaybackAllowed;
736
+ this.summaryLoaded = false;
737
+ this.staffJoined = false;
738
+ this.onControlChanged = (evt) => {
739
+ if (!evt?.callId || evt.callId !== this.callId())
740
+ return;
741
+ this.call.update((c) => {
742
+ if (!c)
743
+ return c;
744
+ const handedOff = evt.aiHandedOff === true || c.aiHandedOff === true;
745
+ const controlMode = evt.controlMode ?? c.controlMode;
746
+ const next = {
747
+ ...c,
748
+ controlMode,
749
+ aiPaused: typeof evt.aiPaused === 'boolean' ? evt.aiPaused : c.aiPaused,
750
+ aiHandedOff: handedOff,
751
+ assigned: evt.assigned !== undefined ? evt.assigned : c.assigned,
752
+ };
753
+ if (controlMode === 'takeover' || handedOff) {
754
+ next.status = 'takeover';
755
+ }
756
+ else if (controlMode === 'ai' || controlMode === 'listen') {
757
+ if (c.status === 'takeover')
758
+ next.status = 'ai';
759
+ }
760
+ return next;
761
+ });
762
+ };
763
+ this.toolTrackKey = toolEventTrackKey;
764
+ effect(() => {
765
+ if (this.stream.callEnded()) {
766
+ void this.staffMedia.disconnect();
767
+ this.staffJoined = false;
768
+ this.loadCallMetadataOnce();
769
+ }
770
+ });
771
+ effect(() => {
772
+ if (this.stream.summaryRequested() && !this.summaryLoaded) {
773
+ this.loadPostCallSummary();
774
+ }
775
+ });
776
+ }
777
+ ngOnInit() {
778
+ this.socket.acquire();
779
+ this.socket.on('call_control_changed', this.onControlChanged);
780
+ this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
781
+ const id = params.get('id') ?? '';
782
+ this.callId.set(id);
783
+ // The softphone lives in a root service, so a session started earlier is still
784
+ // running when this page is re-created.
785
+ this.staffJoined = this.staffMedia.isConnectedTo(id);
786
+ this.loading.set(true);
787
+ this.summary.set(null);
788
+ this.summaryLoaded = false;
789
+ this.stream.stop();
790
+ this.loadCallMetadata();
791
+ });
792
+ }
793
+ /**
794
+ * Leaving the page must never drop the caller. The LiveKit session is owned by the
795
+ * root StaffMediaService and keeps running until End Call or an explicit leave.
796
+ */
797
+ ngOnDestroy() {
798
+ this.socket.off('call_control_changed', this.onControlChanged);
799
+ this.socket.release();
800
+ this.stream.stop();
801
+ }
802
+ /** True when this page's call is the one the softphone is connected to. */
803
+ isMediaForThisCall() {
804
+ return this.staffMedia.activeCallId() === this.callId();
805
+ }
806
+ isTakeover() {
807
+ if (this.mediaMode() === 'takeover' && this.isMediaForThisCall())
808
+ return true;
809
+ const c = this.call();
810
+ if (!c)
811
+ return false;
812
+ if (c.aiHandedOff)
813
+ return true;
814
+ if (c.controlMode === 'listen')
815
+ return false;
816
+ return c.controlMode === 'takeover' || c.status === 'takeover' || !!c.assigned;
817
+ }
818
+ /** After takeover the AI is gone for good — no resume path is offered. */
819
+ isHandedOff() {
820
+ return this.call()?.aiHandedOff === true || this.isTakeover();
821
+ }
822
+ isPaused() {
823
+ const c = this.call();
824
+ return c?.controlMode === 'paused' || !!c?.aiPaused;
825
+ }
826
+ isLive() {
827
+ return !!this.call() && !this.callEnded();
828
+ }
829
+ confidenceLevel(confidence) {
830
+ if (confidence >= 70)
831
+ return 'high';
832
+ if (confidence < 50)
833
+ return 'low';
834
+ return 'mid';
835
+ }
836
+ sentimentPercent(sentiment) {
837
+ const map = {
838
+ calm: 85,
839
+ neutral: 60,
840
+ frustrated: 35,
841
+ urgent: 15,
842
+ };
843
+ return map[sentiment];
844
+ }
845
+ toolStateClass(state) {
846
+ return `live-monitor-page__tool--${state}`;
847
+ }
848
+ isUnknownCaller(call) {
849
+ return isUnknownCaller(call.name, call.matched);
850
+ }
851
+ displayName(call) {
852
+ return this.isUnknownCaller(call) ? 'Unknown Caller' : call.name.trim();
853
+ }
854
+ profileInitials(call) {
855
+ return this.isUnknownCaller(call) ? '?' : call.initials || '?';
856
+ }
857
+ profileMeta(call) {
858
+ return callerMetaLine({
859
+ name: call.name,
860
+ matched: call.matched,
861
+ phone: call.phone,
862
+ dob: call.dob,
863
+ age: call.age ?? ageFromDob(call.dob),
864
+ gender: call.gender,
865
+ });
866
+ }
867
+ goalBadge(call) {
868
+ const raw = (call.goal || call.reason || '').trim();
869
+ if (!raw)
870
+ return null;
871
+ return raw.replace(/[_-]+/g, ' ').toUpperCase();
872
+ }
873
+ takeover() {
874
+ void this.joinMedia('takeover');
875
+ }
876
+ toggleMic() {
877
+ void this.staffMedia.toggleMicrophone();
878
+ }
879
+ transfer() {
880
+ this.api.postTransfer(this.callId()).subscribe(() => this.refreshMetadata());
881
+ }
882
+ endCall() {
883
+ const id = this.callId();
884
+ void this.staffMedia.disconnect().then(() => {
885
+ this.staffJoined = false;
886
+ this.api.postEnd(id).subscribe(() => {
887
+ this.stream.markCompleted();
888
+ this.loadCallMetadataOnce();
889
+ this.loadPostCallSummary();
890
+ });
891
+ });
892
+ }
893
+ sendMessage() {
894
+ const text = this.staffMessage().trim();
895
+ if (!text)
896
+ return;
897
+ this.api.postMessage(this.callId(), text).subscribe(() => {
898
+ this.staffMessage.set('');
899
+ this.refreshMetadata();
900
+ });
901
+ }
902
+ enableSound() {
903
+ void this.staffMedia.startAudioPlayback();
904
+ }
905
+ async joinMedia(mode) {
906
+ if (this.mediaBusy() || this.callEnded())
907
+ return;
908
+ const id = this.callId();
909
+ if (!id)
910
+ return;
911
+ // Check the mic before joining so a blocked browser never leaves the AI paused.
912
+ if (mode === 'takeover' && !this.api.usesMockStreaming()) {
913
+ const blocked = this.staffMedia.microphoneBlockedReason();
914
+ if (blocked) {
915
+ this.toast.show(blocked, 'error', 6000);
916
+ return;
917
+ }
918
+ }
919
+ this.mediaBusy.set(true);
920
+ try {
921
+ const { data } = await firstValueFrom(this.api.postMediaJoin(id, mode));
922
+ this.applyJoinMetadata(data);
923
+ if (!this.api.usesMockStreaming()) {
924
+ await this.staffMedia.join(data);
925
+ }
926
+ this.staffJoined = true;
927
+ this.toast.show(mode === 'takeover' ? 'You are on the call' : 'Listening to the call', 'success');
928
+ }
929
+ catch (err) {
930
+ const message = err instanceof Error ? err.message : `Failed to ${mode === 'takeover' ? 'take over' : 'listen'}`;
931
+ this.toast.show(message, 'error');
932
+ try {
933
+ await this.staffMedia.disconnect();
934
+ await firstValueFrom(this.api.postMediaLeave(id));
935
+ }
936
+ catch {
937
+ /* ignore cleanup errors */
938
+ }
939
+ this.staffJoined = false;
940
+ }
941
+ finally {
942
+ this.mediaBusy.set(false);
943
+ }
944
+ }
945
+ /**
946
+ * Disconnect LiveKit first, then tell the server to drop staff and restore AI.
947
+ * Only valid in listen mode — a taken-over call stays with the human.
948
+ * Does not hang up the patient.
949
+ */
950
+ async leaveMedia(notifyServer) {
951
+ const id = this.callId();
952
+ const wasJoined = this.staffJoined || this.staffMedia.isConnectedTo(id);
953
+ try {
954
+ await this.staffMedia.disconnect();
955
+ }
956
+ catch {
957
+ /* ignore */
958
+ }
959
+ this.staffJoined = false;
960
+ if (!notifyServer || !wasJoined || !id || this.callEnded())
961
+ return;
962
+ this.mediaBusy.set(true);
963
+ try {
964
+ await firstValueFrom(this.api.postResumeAi(id));
965
+ this.call.update((c) => c
966
+ ? {
967
+ ...c,
968
+ status: c.status === 'takeover' ? 'ai' : c.status,
969
+ controlMode: 'ai',
970
+ assigned: null,
971
+ aiPaused: false,
972
+ }
973
+ : c);
974
+ }
975
+ catch (err) {
976
+ const message = err instanceof Error ? err.message : 'Failed to resume AI';
977
+ this.toast.show(message, 'error');
978
+ }
979
+ finally {
980
+ this.mediaBusy.set(false);
981
+ }
982
+ }
983
+ applyJoinMetadata(data) {
984
+ this.call.update((c) => {
985
+ if (!c)
986
+ return c;
987
+ const controlMode = data.controlMode || data.mode;
988
+ return {
989
+ ...c,
990
+ controlMode,
991
+ aiPaused: typeof data.aiPaused === 'boolean' ? data.aiPaused : data.mode === 'takeover',
992
+ aiHandedOff: data.aiHandedOff === true || data.mode === 'takeover' || c.aiHandedOff === true,
993
+ assigned: data.assigned !== undefined && data.assigned !== null
994
+ ? data.assigned
995
+ : data.mode === 'takeover'
996
+ ? c.assigned || 'You'
997
+ : c.assigned,
998
+ status: data.mode === 'takeover' ? 'takeover' : c.status,
999
+ };
1000
+ });
1001
+ }
1002
+ /** Initial load — seeds transcript/tools from REST, then starts socket. */
1003
+ loadCallMetadata() {
1004
+ const id = this.callId();
1005
+ if (!id)
1006
+ return;
1007
+ this.api.getLiveCall(id).subscribe(({ data }) => {
1008
+ this.call.set(data);
1009
+ this.loading.set(false);
1010
+ if (data) {
1011
+ this.stream.seed(data.transcript, data.tools);
1012
+ if (!this.callEnded()) {
1013
+ this.stream.watch(id, true);
1014
+ }
1015
+ }
1016
+ });
1017
+ }
1018
+ /** Metadata-only refresh after staff actions — does not overwrite live transcript/tools. */
1019
+ refreshMetadata() {
1020
+ const id = this.callId();
1021
+ if (!id)
1022
+ return;
1023
+ this.api.getLiveCall(id).subscribe(({ data }) => {
1024
+ if (data)
1025
+ this.call.set(data);
1026
+ });
1027
+ }
1028
+ /** One-time metadata refresh after call ends (no socket). */
1029
+ loadCallMetadataOnce() {
1030
+ const id = this.callId();
1031
+ if (!id)
1032
+ return;
1033
+ this.api.getLiveCall(id).subscribe(({ data }) => {
1034
+ if (data) {
1035
+ this.call.set(data);
1036
+ }
1037
+ });
1038
+ }
1039
+ loadPostCallSummary() {
1040
+ const id = this.callId();
1041
+ if (!id || this.summaryLoaded)
1042
+ return;
1043
+ this.summaryLoading.set(true);
1044
+ this.api.getCallSummary(id).subscribe(({ data }) => {
1045
+ this.summaryLoading.set(false);
1046
+ this.summaryLoaded = true;
1047
+ if (!data)
1048
+ return;
1049
+ this.summary.set(data.summary);
1050
+ this.stream.seed(data.transcript, data.tools);
1051
+ });
1052
+ }
1053
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveMonitorPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1054
+ 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\">\n @if (loading()) {\n <p class=\"live-monitor-page__loading\">Loading call\u2026</p>\n } @else {\n @if (call(); as c) {\n <header class=\"live-monitor-page__header\">\n <a routerLink=\"../live\" class=\"live-monitor-page__back\" aria-label=\"Back to live calls\">\n <eds-icon name=\"arrow_back\" [size]=\"19\" />\n </a>\n\n <div class=\"live-monitor-page__profile\">\n <eds-avatar\n class=\"live-monitor-page__avatar\"\n [initials]=\"profileInitials(c)\"\n [tone]=\"isUnknownCaller(c) ? 'gray' : 'brand'\"\n />\n\n <div class=\"live-monitor-page__profile-text\">\n <div class=\"live-monitor-page__name-row\">\n <h1 class=\"live-monitor-page__name\">{{ displayName(c) }}</h1>\n @if (goalBadge(c); as badge) {\n <span class=\"live-monitor-page__goal-badge\">{{ badge }}</span>\n }\n </div>\n <p class=\"live-monitor-page__profile-meta\">{{ profileMeta(c) }}</p>\n </div>\n </div>\n\n <div class=\"live-monitor-page__header-actions\">\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\"\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\"\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\"\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\"\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\"\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\"\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=\"live-monitor-page__duration\">\n <span class=\"live-monitor-page__duration-dot\" aria-hidden=\"true\"></span>\n {{ c.started | edsElapsed }}\n </span>\n }\n </div>\n </header>\n\n @if (callEnded()) {\n <div class=\"live-monitor-page__completed-banner\" role=\"status\">\n <eds-icon name=\"check_circle\" [size]=\"18\" />\n Call ended\n </div>\n }\n\n <div class=\"live-monitor-page__grid\">\n <div class=\"live-monitor-page__main\">\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\">\n <div class=\"live-monitor-page__summary\">\n <h2 class=\"live-monitor-page__section-title\">Call summary</h2>\n @if (summaryLoading()) {\n <p class=\"live-monitor-page__muted\">Generating summary\u2026</p>\n } @else if (summary()) {\n <p>{{ summary() }}</p>\n } @else {\n <p class=\"live-monitor-page__muted\">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\">\n @if (!callEnded()) {\n <div class=\"live-monitor-page__orb-wrap\" [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\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" />\n </span>\n <span class=\"live-monitor-page__confidence\" [attr.data-level]=\"confidenceLevel(c.confidence)\">\n AI confidence {{ c.confidence }}%\n </span>\n </div>\n\n <section class=\"live-monitor-page__sidebar-block\">\n <h3 class=\"live-monitor-page__section-title\">Patient sentiment</h3>\n <div class=\"live-monitor-page__sentiment-bar\">\n <span\n class=\"live-monitor-page__sentiment-fill\"\n [style.width.%]=\"sentimentPercent(c.sentiment)\"\n [attr.data-sentiment]=\"c.sentiment\"\n ></span>\n </div>\n <span class=\"live-monitor-page__sentiment-label\">{{ c.sentiment }}</span>\n </section>\n }\n\n <section class=\"live-monitor-page__sidebar-block live-monitor-page__sidebar-block--grow\">\n <h3 class=\"live-monitor-page__section-title\">Goal Timeline</h3>\n <p class=\"live-monitor-page__goal\">{{ c.goal || '\u2014' }}</p>\n <ul class=\"live-monitor-page__tools\">\n @for (tool of liveTools(); track toolTrackKey(tool, $index)) {\n <li [class]=\"toolStateClass(tool.state)\">\n <span class=\"live-monitor-page__tool-time\">{{ tool.t }}</span>\n <div class=\"live-monitor-page__tool-body\">\n <span class=\"live-monitor-page__tool-name\">{{ tool.name }}</span>\n <span class=\"live-monitor-page__tool-result\">{{ tool.result }}</span>\n </div>\n @if (tool.state === 'active') {\n <eds-icon name=\"sync\" [size]=\"14\" class=\"live-monitor-page__tool-spin\" />\n }\n </li>\n } @empty {\n <li class=\"live-monitor-page__tools-empty\">No tool activity yet.</li>\n }\n </ul>\n </section>\n </aside>\n </div>\n } @else {\n <p class=\"live-monitor-page__loading\">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{display:flex;flex-direction:column;height:100%;min-height:0;padding:var(--eds-space-5);box-sizing:border-box;color:var(--eds-text-primary, #18181b);background:var(--eds-surface-l1, #f4f4f5);overflow:hidden}.live-monitor-page__loading{font:400 13px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__header{display:flex;align-items:center;gap:12px;margin-bottom:var(--eds-space-4);flex-wrap:wrap;flex-shrink:0}.live-monitor-page__back{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--eds-radius-md);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);color:var(--eds-text-secondary, #52525b);text-decoration:none;flex-shrink:0}.live-monitor-page__back:hover{background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__profile{flex:1;display:flex;align-items:center;gap:12px;min-width:220px}.live-monitor-page__avatar{flex-shrink:0}.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__profile-text{min-width:0}.live-monitor-page__name-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.live-monitor-page__name{margin:0;font:700 18px/22px var(--eds-font-sans);color:var(--eds-text-primary, #18181b)}.live-monitor-page__goal-badge{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font:700 11px/1 var(--eds-font-sans);letter-spacing:.02em;white-space:nowrap}.live-monitor-page__profile-meta{margin:4px 0 0;font:400 12px/1.3 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__header-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.live-monitor-page__header-btn{display:inline-flex;align-items:center;gap:6px;height:38px;padding:0 14px;border-radius:8px;font:600 13px/1 var(--eds-font-sans);cursor:pointer;white-space:nowrap}.live-monitor-page__header-btn:disabled{opacity:.55;cursor:not-allowed}.live-monitor-page__header-btn--danger{background:#fff;border:1px solid #fca5a5;color:#dc2626}.live-monitor-page__header-btn--danger:hover:not(:disabled){background:#fef2f2}.live-monitor-page__header-btn--primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.live-monitor-page__header-btn--primary:hover:not(:disabled){background:#1d4ed8}.live-monitor-page__header-btn--ghost{background:#fff;border:1px solid var(--eds-outline, #e5e5e5);color:var(--eds-text-primary, #41464c)}.live-monitor-page__header-btn--ghost:hover:not(:disabled){background:var(--eds-bg-subtle, #f7f9fa)}.live-monitor-page__media-pill{display:inline-flex;align-items:center;height:28px;padding:0 10px;border-radius:999px;font:600 11px/1 var(--eds-font-sans);letter-spacing:.02em;background:var(--eds-bg-subtle, #f7f9fa);color:var(--eds-text-secondary, #53575c);border:1px solid var(--eds-outline, #e5e5e5)}.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{display:inline-flex;align-items:center;gap:8px;font:700 18px/1 var(--eds-font-mono);color:var(--eds-text-primary, #18181b)}.live-monitor-page__duration--footer{margin-left:auto;font-size:16px}.live-monitor-page__duration-dot{width:9px;height:9px;border-radius:50%;background:var(--eds-green-500, #22c55e);box-shadow:0 0 0 3px #22c55e24}.live-monitor-page__completed-banner{display:flex;align-items:center;gap:8px;padding:10px 14px;margin-bottom:var(--eds-space-4);border-radius:var(--eds-radius-lg);background:#ecfdf5;color:#047857;font:400 13px/1.4 var(--eds-font-body)}.live-monitor-page__grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(240px,3fr);gap:var(--eds-space-4);flex:1;min-height:0;align-items:stretch}.live-monitor-page__main{display:flex;flex-direction:column;gap:var(--eds-space-4);min-height:0;height:100%}.live-monitor-page__main-tabs{display:flex;gap:4px;padding:4px;border-radius:var(--eds-radius-md);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);width:fit-content}.live-monitor-page__main-tab{border:none;background:transparent;color:var(--eds-text-secondary, #71717a);font:600 12px/1 var(--eds-font-body);padding:8px 14px;border-radius:var(--eds-radius-sm);cursor:pointer}.live-monitor-page__main-tab--active{background:var(--eds-surface-l1, #f4f4f5);color:var(--eds-text-primary, #18181b)}.live-monitor-page__api-logs{flex:1;min-height:280px;padding:16px;background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);border-radius:var(--eds-radius-lg)}.live-monitor-page__post-call{display:flex;flex-direction:column;gap:var(--eds-space-4)}.live-monitor-page__summary{padding:16px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);font:400 13px/1.5 var(--eds-font-body);color:var(--eds-text-secondary, #3f3f46)}.live-monitor-page__muted{color:var(--eds-text-secondary, #71717a);margin:0}.live-monitor-page__sidebar{width:auto;min-width:0;display:flex;flex-direction:column;gap:12px;min-height:0;height:100%;overflow:auto}.live-monitor-page__sidebar-block{padding:14px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7)}.live-monitor-page__section-title{margin:0 0 10px;font-family:var(--eds-font-inter);font-size:13px;font-weight:600;line-height:1.2;letter-spacing:normal;text-transform:none;color:var(--eds-text-table-header, #4d5573)}.live-monitor-page__orb-wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;width:100%;min-height:160px;box-sizing:border-box;padding:20px 16px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);position:relative}.live-monitor-page__orb{width:48px;height:48px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500));animation:eds-breathe 3s ease-in-out infinite;position:relative;z-index:1}.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{font:500 11px/1.3 var(--eds-font-mono);color:var(--eds-text-secondary, #71717a);text-align:center;max-width:100%}.live-monitor-page__confidence[data-level=high]{color:#047857}.live-monitor-page__confidence[data-level=low]{color:#b45309}.live-monitor-page__sentiment-bar{height:5px;background:var(--eds-surface-l1, #e4e4e7);border-radius:3px;overflow:hidden;margin-bottom:6px}.live-monitor-page__sentiment-fill{display:block;height:100%;border-radius:3px;background:var(--eds-green-500, #22c55e);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__sentiment-label{font:500 11px/1.2 var(--eds-font-mono);color:var(--eds-text-secondary, #71717a);text-transform:capitalize}.live-monitor-page__tools{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;flex:1;min-height:0;overflow:auto}.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-time{font:500 11px/1.2 var(--eds-font-mono);color:var(--eds-text-secondary, #a1a1aa);white-space:nowrap}.live-monitor-page__tool-body{display:flex;flex-direction:column;gap:2px;min-width:0}.live-monitor-page__tool-name{font:600 13px/1.25 var(--eds-font-sans);color:var(--eds-text-primary, #18181b);word-break:break-word}.live-monitor-page__tool-result{font:400 12px/1.35 var(--eds-font-body);color:var(--eds-text-secondary, #71717a);word-break:break-word}.live-monitor-page__tool-tech{font:500 9px/1 var(--eds-font-mono);color:var(--eds-text-secondary, #a1a1aa)}.live-monitor-page__tool-spin{animation:eds-spin 1s linear infinite;color:var(--eds-brand-500);grid-column:1/-1;justify-self:end}.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;font:400 11px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__goal{margin:0 0 12px;font:400 13px/1.45 var(--eds-font-body);color:var(--eds-text-secondary, #3f3f46)}.live-monitor-page__sidebar-block--grow{flex:1;min-height:0;display:flex;flex-direction:column}@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 }); }
1055
+ }
1056
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LiveMonitorPageComponent, decorators: [{
1057
+ type: Component,
1058
+ args: [{ selector: 'eds-live-monitor-page', imports: [
1059
+ RouterLink,
1060
+ FormsModule,
1061
+ EdsAvatarComponent,
1062
+ EdsIconComponent,
1063
+ EdsElapsedPipe,
1064
+ LiveTranscriptPanelComponent,
1065
+ CallRecordingPlayerComponent,
1066
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page live-monitor-page\">\n @if (loading()) {\n <p class=\"live-monitor-page__loading\">Loading call\u2026</p>\n } @else {\n @if (call(); as c) {\n <header class=\"live-monitor-page__header\">\n <a routerLink=\"../live\" class=\"live-monitor-page__back\" aria-label=\"Back to live calls\">\n <eds-icon name=\"arrow_back\" [size]=\"19\" />\n </a>\n\n <div class=\"live-monitor-page__profile\">\n <eds-avatar\n class=\"live-monitor-page__avatar\"\n [initials]=\"profileInitials(c)\"\n [tone]=\"isUnknownCaller(c) ? 'gray' : 'brand'\"\n />\n\n <div class=\"live-monitor-page__profile-text\">\n <div class=\"live-monitor-page__name-row\">\n <h1 class=\"live-monitor-page__name\">{{ displayName(c) }}</h1>\n @if (goalBadge(c); as badge) {\n <span class=\"live-monitor-page__goal-badge\">{{ badge }}</span>\n }\n </div>\n <p class=\"live-monitor-page__profile-meta\">{{ profileMeta(c) }}</p>\n </div>\n </div>\n\n <div class=\"live-monitor-page__header-actions\">\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\"\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\"\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\"\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\"\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\"\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\"\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=\"live-monitor-page__duration\">\n <span class=\"live-monitor-page__duration-dot\" aria-hidden=\"true\"></span>\n {{ c.started | edsElapsed }}\n </span>\n }\n </div>\n </header>\n\n @if (callEnded()) {\n <div class=\"live-monitor-page__completed-banner\" role=\"status\">\n <eds-icon name=\"check_circle\" [size]=\"18\" />\n Call ended\n </div>\n }\n\n <div class=\"live-monitor-page__grid\">\n <div class=\"live-monitor-page__main\">\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\">\n <div class=\"live-monitor-page__summary\">\n <h2 class=\"live-monitor-page__section-title\">Call summary</h2>\n @if (summaryLoading()) {\n <p class=\"live-monitor-page__muted\">Generating summary\u2026</p>\n } @else if (summary()) {\n <p>{{ summary() }}</p>\n } @else {\n <p class=\"live-monitor-page__muted\">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\">\n @if (!callEnded()) {\n <div class=\"live-monitor-page__orb-wrap\" [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\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" />\n </span>\n <span class=\"live-monitor-page__confidence\" [attr.data-level]=\"confidenceLevel(c.confidence)\">\n AI confidence {{ c.confidence }}%\n </span>\n </div>\n\n <section class=\"live-monitor-page__sidebar-block\">\n <h3 class=\"live-monitor-page__section-title\">Patient sentiment</h3>\n <div class=\"live-monitor-page__sentiment-bar\">\n <span\n class=\"live-monitor-page__sentiment-fill\"\n [style.width.%]=\"sentimentPercent(c.sentiment)\"\n [attr.data-sentiment]=\"c.sentiment\"\n ></span>\n </div>\n <span class=\"live-monitor-page__sentiment-label\">{{ c.sentiment }}</span>\n </section>\n }\n\n <section class=\"live-monitor-page__sidebar-block live-monitor-page__sidebar-block--grow\">\n <h3 class=\"live-monitor-page__section-title\">Goal Timeline</h3>\n <p class=\"live-monitor-page__goal\">{{ c.goal || '\u2014' }}</p>\n <ul class=\"live-monitor-page__tools\">\n @for (tool of liveTools(); track toolTrackKey(tool, $index)) {\n <li [class]=\"toolStateClass(tool.state)\">\n <span class=\"live-monitor-page__tool-time\">{{ tool.t }}</span>\n <div class=\"live-monitor-page__tool-body\">\n <span class=\"live-monitor-page__tool-name\">{{ tool.name }}</span>\n <span class=\"live-monitor-page__tool-result\">{{ tool.result }}</span>\n </div>\n @if (tool.state === 'active') {\n <eds-icon name=\"sync\" [size]=\"14\" class=\"live-monitor-page__tool-spin\" />\n }\n </li>\n } @empty {\n <li class=\"live-monitor-page__tools-empty\">No tool activity yet.</li>\n }\n </ul>\n </section>\n </aside>\n </div>\n } @else {\n <p class=\"live-monitor-page__loading\">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{display:flex;flex-direction:column;height:100%;min-height:0;padding:var(--eds-space-5);box-sizing:border-box;color:var(--eds-text-primary, #18181b);background:var(--eds-surface-l1, #f4f4f5);overflow:hidden}.live-monitor-page__loading{font:400 13px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__header{display:flex;align-items:center;gap:12px;margin-bottom:var(--eds-space-4);flex-wrap:wrap;flex-shrink:0}.live-monitor-page__back{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--eds-radius-md);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);color:var(--eds-text-secondary, #52525b);text-decoration:none;flex-shrink:0}.live-monitor-page__back:hover{background:var(--eds-surface-l1, #f4f4f5)}.live-monitor-page__profile{flex:1;display:flex;align-items:center;gap:12px;min-width:220px}.live-monitor-page__avatar{flex-shrink:0}.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__profile-text{min-width:0}.live-monitor-page__name-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.live-monitor-page__name{margin:0;font:700 18px/22px var(--eds-font-sans);color:var(--eds-text-primary, #18181b)}.live-monitor-page__goal-badge{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font:700 11px/1 var(--eds-font-sans);letter-spacing:.02em;white-space:nowrap}.live-monitor-page__profile-meta{margin:4px 0 0;font:400 12px/1.3 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__header-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.live-monitor-page__header-btn{display:inline-flex;align-items:center;gap:6px;height:38px;padding:0 14px;border-radius:8px;font:600 13px/1 var(--eds-font-sans);cursor:pointer;white-space:nowrap}.live-monitor-page__header-btn:disabled{opacity:.55;cursor:not-allowed}.live-monitor-page__header-btn--danger{background:#fff;border:1px solid #fca5a5;color:#dc2626}.live-monitor-page__header-btn--danger:hover:not(:disabled){background:#fef2f2}.live-monitor-page__header-btn--primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.live-monitor-page__header-btn--primary:hover:not(:disabled){background:#1d4ed8}.live-monitor-page__header-btn--ghost{background:#fff;border:1px solid var(--eds-outline, #e5e5e5);color:var(--eds-text-primary, #41464c)}.live-monitor-page__header-btn--ghost:hover:not(:disabled){background:var(--eds-bg-subtle, #f7f9fa)}.live-monitor-page__media-pill{display:inline-flex;align-items:center;height:28px;padding:0 10px;border-radius:999px;font:600 11px/1 var(--eds-font-sans);letter-spacing:.02em;background:var(--eds-bg-subtle, #f7f9fa);color:var(--eds-text-secondary, #53575c);border:1px solid var(--eds-outline, #e5e5e5)}.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{display:inline-flex;align-items:center;gap:8px;font:700 18px/1 var(--eds-font-mono);color:var(--eds-text-primary, #18181b)}.live-monitor-page__duration--footer{margin-left:auto;font-size:16px}.live-monitor-page__duration-dot{width:9px;height:9px;border-radius:50%;background:var(--eds-green-500, #22c55e);box-shadow:0 0 0 3px #22c55e24}.live-monitor-page__completed-banner{display:flex;align-items:center;gap:8px;padding:10px 14px;margin-bottom:var(--eds-space-4);border-radius:var(--eds-radius-lg);background:#ecfdf5;color:#047857;font:400 13px/1.4 var(--eds-font-body)}.live-monitor-page__grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(240px,3fr);gap:var(--eds-space-4);flex:1;min-height:0;align-items:stretch}.live-monitor-page__main{display:flex;flex-direction:column;gap:var(--eds-space-4);min-height:0;height:100%}.live-monitor-page__main-tabs{display:flex;gap:4px;padding:4px;border-radius:var(--eds-radius-md);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);width:fit-content}.live-monitor-page__main-tab{border:none;background:transparent;color:var(--eds-text-secondary, #71717a);font:600 12px/1 var(--eds-font-body);padding:8px 14px;border-radius:var(--eds-radius-sm);cursor:pointer}.live-monitor-page__main-tab--active{background:var(--eds-surface-l1, #f4f4f5);color:var(--eds-text-primary, #18181b)}.live-monitor-page__api-logs{flex:1;min-height:280px;padding:16px;background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);border-radius:var(--eds-radius-lg)}.live-monitor-page__post-call{display:flex;flex-direction:column;gap:var(--eds-space-4)}.live-monitor-page__summary{padding:16px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);font:400 13px/1.5 var(--eds-font-body);color:var(--eds-text-secondary, #3f3f46)}.live-monitor-page__muted{color:var(--eds-text-secondary, #71717a);margin:0}.live-monitor-page__sidebar{width:auto;min-width:0;display:flex;flex-direction:column;gap:12px;min-height:0;height:100%;overflow:auto}.live-monitor-page__sidebar-block{padding:14px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7)}.live-monitor-page__section-title{margin:0 0 10px;font-family:var(--eds-font-inter);font-size:13px;font-weight:600;line-height:1.2;letter-spacing:normal;text-transform:none;color:var(--eds-text-table-header, #4d5573)}.live-monitor-page__orb-wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;width:100%;min-height:160px;box-sizing:border-box;padding:20px 16px;border-radius:var(--eds-radius-lg);background:var(--eds-surface-l0, #fff);border:1px solid var(--eds-border-default, #e4e4e7);position:relative}.live-monitor-page__orb{width:48px;height:48px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;background:linear-gradient(135deg,var(--eds-brand-500),var(--eds-violet-500));animation:eds-breathe 3s ease-in-out infinite;position:relative;z-index:1}.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{font:500 11px/1.3 var(--eds-font-mono);color:var(--eds-text-secondary, #71717a);text-align:center;max-width:100%}.live-monitor-page__confidence[data-level=high]{color:#047857}.live-monitor-page__confidence[data-level=low]{color:#b45309}.live-monitor-page__sentiment-bar{height:5px;background:var(--eds-surface-l1, #e4e4e7);border-radius:3px;overflow:hidden;margin-bottom:6px}.live-monitor-page__sentiment-fill{display:block;height:100%;border-radius:3px;background:var(--eds-green-500, #22c55e);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__sentiment-label{font:500 11px/1.2 var(--eds-font-mono);color:var(--eds-text-secondary, #71717a);text-transform:capitalize}.live-monitor-page__tools{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;flex:1;min-height:0;overflow:auto}.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-time{font:500 11px/1.2 var(--eds-font-mono);color:var(--eds-text-secondary, #a1a1aa);white-space:nowrap}.live-monitor-page__tool-body{display:flex;flex-direction:column;gap:2px;min-width:0}.live-monitor-page__tool-name{font:600 13px/1.25 var(--eds-font-sans);color:var(--eds-text-primary, #18181b);word-break:break-word}.live-monitor-page__tool-result{font:400 12px/1.35 var(--eds-font-body);color:var(--eds-text-secondary, #71717a);word-break:break-word}.live-monitor-page__tool-tech{font:500 9px/1 var(--eds-font-mono);color:var(--eds-text-secondary, #a1a1aa)}.live-monitor-page__tool-spin{animation:eds-spin 1s linear infinite;color:var(--eds-brand-500);grid-column:1/-1;justify-self:end}.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;font:400 11px/1.4 var(--eds-font-body);color:var(--eds-text-secondary, #71717a)}.live-monitor-page__goal{margin:0 0 12px;font:400 13px/1.45 var(--eds-font-body);color:var(--eds-text-secondary, #3f3f46)}.live-monitor-page__sidebar-block--grow{flex:1;min-height:0;display:flex;flex-direction:column}@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"] }]
1067
+ }], ctorParameters: () => [] });
1068
+
1069
+ export { LiveMonitorPageComponent };
1070
+ //# sourceMappingURL=edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map