@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
@@ -1,61 +1,8 @@
1
- import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, signal, Injectable, inject, ChangeDetectionStrategy, Component, input, computed, EventEmitter, HostListener, Output, Input } from '@angular/core';
3
- import { RouterLink, RouterLinkActive, Router, NavigationEnd, RouterOutlet } from '@angular/router';
4
- import { of, delay, throwError, map, catchError, forkJoin, switchMap, tap, finalize } from 'rxjs';
5
1
  import { HttpClient, HttpParams } from '@angular/common/http';
6
- import { filter } from 'rxjs/operators';
7
-
8
- const VOICE_RECEPTIONIST_CONFIG = new InjectionToken('VOICE_RECEPTIONIST_CONFIG');
9
- const VOICE_RECEPTIONIST_SESSION = new InjectionToken('VOICE_RECEPTIONIST_SESSION', { factory: () => null });
10
-
11
- function provideVoiceReceptionist(options) {
12
- const { session = null, ...config } = options;
13
- return makeEnvironmentProviders([
14
- { provide: VOICE_RECEPTIONIST_CONFIG, useValue: config },
15
- { provide: VOICE_RECEPTIONIST_SESSION, useValue: session },
16
- ]);
17
- }
18
-
19
- class ToastService {
20
- constructor() {
21
- this.nextId = 1;
22
- this.timers = new Map();
23
- this._toasts = signal([]);
24
- this.toasts = this._toasts.asReadonly();
25
- }
26
- show(text, variant = 'info', durationMs = 3500) {
27
- const id = this.nextId++;
28
- this._toasts.update((list) => [...list, { id, text, variant }]);
29
- const timer = setTimeout(() => this.dismiss(id), durationMs);
30
- this.timers.set(id, timer);
31
- }
32
- dismiss(id) {
33
- const timer = this.timers.get(id);
34
- if (timer) {
35
- clearTimeout(timer);
36
- this.timers.delete(id);
37
- }
38
- this._toasts.update((list) => list.filter((t) => t.id !== id));
39
- }
40
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
41
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, providedIn: 'root' }); }
42
- }
43
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, decorators: [{
44
- type: Injectable,
45
- args: [{ providedIn: 'root' }]
46
- }] });
47
-
48
- class ToastContainerComponent {
49
- constructor() {
50
- this.toastService = inject(ToastService);
51
- }
52
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: ToastContainerComponent, isStandalone: true, selector: "app-toast-container", ngImport: i0, template: "<div class=\"toast-stack\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div class=\"toast\" [class]=\"'toast-' + toast.variant\" role=\"status\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n @switch (toast.variant) {\n @case ('success') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n @case ('error') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"13\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n @default {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n }\n </span>\n <span class=\"toast-text\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"toast-dismiss\"\n (click)=\"toastService.dismiss(toast.id)\"\n aria-label=\"Dismiss notification\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;bottom:24px;right:24px;z-index:1100;pointer-events:none}.toast-stack{display:flex;flex-direction:column;gap:10px;max-width:380px}.toast{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--color-text);animation:slide-in .16s ease}.toast-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;flex-shrink:0}.toast-info .toast-icon{background:var(--color-primary-soft);color:var(--color-primary)}.toast-success .toast-icon{background:var(--color-success-soft);color:var(--color-success)}.toast-error .toast-icon{background:var(--color-danger-soft);color:var(--color-danger)}.toast-text{flex:1;line-height:1.45}.toast-dismiss{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--color-surface-alt);color:var(--color-text)}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
54
- }
55
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, decorators: [{
56
- type: Component,
57
- args: [{ selector: 'app-toast-container', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toast-stack\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div class=\"toast\" [class]=\"'toast-' + toast.variant\" role=\"status\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n @switch (toast.variant) {\n @case ('success') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n @case ('error') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"13\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n @default {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n }\n </span>\n <span class=\"toast-text\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"toast-dismiss\"\n (click)=\"toastService.dismiss(toast.id)\"\n aria-label=\"Dismiss notification\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;bottom:24px;right:24px;z-index:1100;pointer-events:none}.toast-stack{display:flex;flex-direction:column;gap:10px;max-width:380px}.toast{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--color-text);animation:slide-in .16s ease}.toast-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;flex-shrink:0}.toast-info .toast-icon{background:var(--color-primary-soft);color:var(--color-primary)}.toast-success .toast-icon{background:var(--color-success-soft);color:var(--color-success)}.toast-error .toast-icon{background:var(--color-danger-soft);color:var(--color-danger)}.toast-text{flex:1;line-height:1.45}.toast-dismiss{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--color-surface-alt);color:var(--color-text)}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"] }]
58
- }] });
2
+ import * as i0 from '@angular/core';
3
+ import { signal, Injectable, inject } from '@angular/core';
4
+ import { of, delay, throwError, map, catchError, forkJoin, switchMap } from 'rxjs';
5
+ import { V as VOICE_RECEPTIONIST_CONFIG } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs';
59
6
 
60
7
  /** Build URL helpers from a config (or raw base URL). Safe outside injection context. */
61
8
  function createVoiceReceptionistPaths(configOrBase) {
@@ -87,7 +34,383 @@ function extractApiRows(body) {
87
34
  return [];
88
35
  }
89
36
 
90
- const WELCOME_MESSAGE_MAX = 500;
37
+ function mapOutboundPatientHit(raw) {
38
+ if (!raw || typeof raw !== 'object')
39
+ return null;
40
+ const r = raw;
41
+ const patientId = String(r['patientId'] ?? r['patient_id'] ?? r['id'] ?? '').trim();
42
+ if (!patientId)
43
+ return null;
44
+ const firstName = String(r['firstName'] ?? r['first_name'] ?? '').trim();
45
+ const lastName = String(r['lastName'] ?? r['last_name'] ?? '').trim();
46
+ const full = String(r['fullName'] ?? r['full_name'] ?? r['name'] ?? '').trim() ||
47
+ [firstName, lastName].filter(Boolean).join(' ');
48
+ const phone = String(r['mobilePhone'] ?? r['phone'] ?? r['mobile_phone'] ?? '').trim() || null;
49
+ const dobRaw = String(r['dateOfBirth'] ?? r['dob'] ?? '').trim();
50
+ return {
51
+ patientId,
52
+ firstName,
53
+ lastName,
54
+ fullName: full || 'Unknown patient',
55
+ phone,
56
+ dob: dobRaw ? dobRaw.slice(0, 10) : null,
57
+ };
58
+ }
59
+ function mapOutboundAppointmentHit(raw) {
60
+ if (!raw || typeof raw !== 'object')
61
+ return null;
62
+ const r = raw;
63
+ const appointmentId = String(r['appointmentId'] ?? r['appointment_id'] ?? r['id'] ?? '').trim();
64
+ if (!appointmentId)
65
+ return null;
66
+ const providerObj = r['provider'] && typeof r['provider'] === 'object'
67
+ ? r['provider']
68
+ : null;
69
+ const locationObj = r['location'] && typeof r['location'] === 'object'
70
+ ? r['location']
71
+ : null;
72
+ const visitObj = r['visitType'] && typeof r['visitType'] === 'object'
73
+ ? r['visitType']
74
+ : null;
75
+ const providerName = String(r['providerName'] ??
76
+ r['provider_name'] ??
77
+ providerObj?.['fullName'] ??
78
+ [providerObj?.['firstName'], providerObj?.['lastName']].filter(Boolean).join(' ') ??
79
+ '').trim() || null;
80
+ const locationName = String(r['locationName'] ?? r['location_name'] ?? locationObj?.['locationName'] ?? '').trim() || null;
81
+ const visitTypeRaw = r['visitType'];
82
+ const visitType = (typeof visitTypeRaw === 'string' ? visitTypeRaw.trim() : '') ||
83
+ String(r['visitTypeName'] ??
84
+ r['visit_type'] ??
85
+ visitObj?.['visitTypeName'] ??
86
+ visitObj?.['name'] ??
87
+ '').trim() ||
88
+ null;
89
+ return {
90
+ appointmentId,
91
+ startTime: String(r['startTime'] ?? r['start_time'] ?? '').trim() || null,
92
+ endTime: String(r['endTime'] ?? r['end_time'] ?? '').trim() || null,
93
+ providerName,
94
+ locationName,
95
+ status: String(r['appointmentStatus'] ?? r['status'] ?? r['visitStatus'] ?? '').trim() || null,
96
+ visitType,
97
+ };
98
+ }
99
+ /** Best-effort E.164 for US/local numbers used in dialer. */
100
+ function toE164Phone(raw) {
101
+ const digits = String(raw ?? '').replace(/\D/g, '');
102
+ if (!digits)
103
+ return '';
104
+ if (String(raw ?? '').trim().startsWith('+') && /^\+[1-9]\d{6,14}$/.test(String(raw).trim())) {
105
+ return String(raw).trim();
106
+ }
107
+ if (digits.length === 10)
108
+ return `+1${digits}`;
109
+ if (digits.length === 11 && digits.startsWith('1'))
110
+ return `+${digits}`;
111
+ if (digits.length >= 8)
112
+ return `+${digits}`;
113
+ return String(raw ?? '').trim();
114
+ }
115
+
116
+ function mapStaffMediaJoinResult(raw) {
117
+ if (!raw || typeof raw !== 'object')
118
+ return null;
119
+ const r = raw;
120
+ const data = r['data'] && typeof r['data'] === 'object'
121
+ ? r['data']
122
+ : r;
123
+ const livekitUrl = String(data['livekitUrl'] ?? data['livekit_url'] ?? data['url'] ?? '').trim();
124
+ const clientToken = String(data['clientToken'] ?? data['client_token'] ?? data['token'] ?? '').trim();
125
+ const modeRaw = String(data['mode'] ?? 'takeover').toLowerCase();
126
+ const mode = modeRaw === 'listen' ? 'listen' : 'takeover';
127
+ if (!livekitUrl || !clientToken)
128
+ return null;
129
+ return {
130
+ ok: data['ok'] !== false,
131
+ callId: String(data['callId'] ?? data['call_id'] ?? ''),
132
+ mode,
133
+ provider: String(data['provider'] ?? 'livekit'),
134
+ livekitUrl,
135
+ clientToken,
136
+ tokenExpiresAt: strOrNull$1(data['tokenExpiresAt'] ?? data['token_expires_at']),
137
+ conversationId: strOrNull$1(data['conversationId'] ?? data['conversation_id']),
138
+ memberId: strOrNull$1(data['memberId'] ?? data['member_id']),
139
+ roomName: strOrNull$1(data['roomName'] ?? data['room_name']),
140
+ iceServers: Array.isArray(data['iceServers']) ? data['iceServers'] : undefined,
141
+ controlMode: strOrNull$1(data['controlMode'] ?? data['control_mode']),
142
+ aiPaused: typeof data['aiPaused'] === 'boolean'
143
+ ? data['aiPaused']
144
+ : typeof data['ai_paused'] === 'boolean'
145
+ ? data['ai_paused']
146
+ : mode === 'takeover',
147
+ aiHandedOff: typeof data['aiHandedOff'] === 'boolean'
148
+ ? data['aiHandedOff']
149
+ : mode === 'takeover',
150
+ assigned: strOrNull$1(data['assigned']),
151
+ };
152
+ }
153
+ function strOrNull$1(value) {
154
+ const s = String(value ?? '').trim();
155
+ return s || null;
156
+ }
157
+
158
+ function mapAgentActivity(raw, index = 0) {
159
+ if (!raw || typeof raw !== 'object') {
160
+ return {
161
+ id: `activity-${index}`,
162
+ type: 'unknown',
163
+ occurredAt: new Date().toISOString(),
164
+ title: 'Activity',
165
+ detail: '',
166
+ };
167
+ }
168
+ const r = raw;
169
+ const nestedPatient = asRecord(r['patient']);
170
+ const nestedProvider = asRecord(r['provider'] ?? r['practitioner'] ?? r['doctor']);
171
+ const type = String(r['type'] ?? r['activityType'] ?? 'unknown');
172
+ const patientName = strOrNull(r['patientName'] ?? r['patient_name']) ?? strOrNull(nestedPatient?.['name']);
173
+ const providerName = strOrNull(r['providerName'] ??
174
+ r['provider_name'] ??
175
+ r['practitionerName'] ??
176
+ r['practitioner_name'] ??
177
+ r['doctorName'] ??
178
+ r['doctor_name']) ?? strOrNull(nestedProvider?.['name']);
179
+ const detail = String(r['detail'] ?? r['description'] ?? '').trim();
180
+ const parsed = parseActivityDetail(detail);
181
+ const insurance = resolveInsurance(r, nestedPatient);
182
+ const visitType = strOrNull(r['visitType'] ?? r['visit_type'] ?? r['category'] ?? r['reason']) ??
183
+ parsed.visitType;
184
+ const startAt = strOrNull(r['startAt'] ?? r['start_at'] ?? r['appointmentStart'] ?? r['appointment_start']) ??
185
+ parsed.startAt;
186
+ const endAt = strOrNull(r['endAt'] ?? r['end_at'] ?? r['appointmentEnd'] ?? r['appointment_end']);
187
+ const practiceName = strOrNull(r['practiceName'] ?? r['practice_name'] ?? r['location'] ?? r['clinic']) ??
188
+ parsed.practiceName;
189
+ return {
190
+ id: String(r['id'] ?? r['activityId'] ?? `activity-${index}`),
191
+ type,
192
+ occurredAt: String(r['occurredAt'] ?? r['occurred_at'] ?? r['createdAt'] ?? ''),
193
+ title: String(r['title'] ?? r['summary'] ?? '').trim() || activityTypeLabel(type),
194
+ detail,
195
+ patientId: strOrNull(r['patientId'] ?? r['patient_id'] ?? nestedPatient?.['id']),
196
+ patientName,
197
+ appointmentId: strOrNull(r['appointmentId'] ?? r['appointment_id']),
198
+ taskId: strOrNull(r['taskId'] ?? r['task_id']),
199
+ callId: strOrNull(r['callId'] ?? r['call_id']),
200
+ startAt,
201
+ endAt,
202
+ durationMinutes: numOrNull(r['durationMinutes'] ?? r['duration_minutes'] ?? r['duration']),
203
+ visitType,
204
+ practiceName,
205
+ patientDob: strOrNull(r['patientDob'] ?? r['patient_dob'] ?? r['dob'] ?? nestedPatient?.['dob']),
206
+ patientAge: numOrNull(r['patientAge'] ?? r['patient_age'] ?? r['age'] ?? nestedPatient?.['age']),
207
+ patientGender: normalizeGender(r['patientGender'] ?? r['patient_gender'] ?? r['gender'] ?? nestedPatient?.['gender']),
208
+ patientInitials: strOrNull(r['patientInitials'] ??
209
+ r['patient_initials'] ??
210
+ r['initials'] ??
211
+ nestedPatient?.['initials']) ?? initialsFromName(patientName),
212
+ patientMrn: strOrNull(r['patientMrn'] ??
213
+ r['patient_mrn'] ??
214
+ r['mrn'] ??
215
+ r['MRN'] ??
216
+ nestedPatient?.['mrn'] ??
217
+ nestedPatient?.['MRN']),
218
+ hasInsurance: insurance.hasInsurance,
219
+ insuranceName: insurance.name,
220
+ providerId: strOrNull(r['providerId'] ?? r['provider_id'] ?? nestedProvider?.['id']),
221
+ providerName,
222
+ providerInitials: strOrNull(r['providerInitials'] ??
223
+ r['provider_initials'] ??
224
+ nestedProvider?.['initials']) ?? initialsFromName(providerName),
225
+ providerSpecialty: strOrNull(r['providerSpecialty'] ??
226
+ r['provider_specialty'] ??
227
+ r['specialty'] ??
228
+ nestedProvider?.['specialty'] ??
229
+ nestedProvider?.['speciality']),
230
+ priority: normalizePriority(r['priority'] ?? r['taskPriority'] ?? r['task_priority']),
231
+ };
232
+ }
233
+ const INSURED_WORDS = ['active', 'verified', 'covered', 'insured', 'yes', 'true', 'eligible'];
234
+ const UNINSURED_WORDS = [
235
+ 'none',
236
+ 'no',
237
+ 'false',
238
+ 'uninsured',
239
+ 'self_pay',
240
+ 'self-pay',
241
+ 'selfpay',
242
+ 'inactive',
243
+ 'expired',
244
+ 'missing',
245
+ ];
246
+ /**
247
+ * Coverage can arrive as a boolean, a status string, a payer name or a nested object.
248
+ * Unknown stays null so the UI can distinguish "no coverage" from "never told us".
249
+ */
250
+ function resolveInsurance(r, patient) {
251
+ const nested = asRecord(r['insurance'] ?? patient?.['insurance']);
252
+ const name = strOrNull(r['insuranceName'] ??
253
+ r['insurance_name'] ??
254
+ r['payerName'] ??
255
+ r['payer_name'] ??
256
+ r['payer'] ??
257
+ patient?.['insuranceName'] ??
258
+ patient?.['insurance_name'] ??
259
+ nested?.['name'] ??
260
+ nested?.['payer'] ??
261
+ nested?.['planName']) ?? (typeof r['insurance'] === 'string' ? strOrNull(r['insurance']) : null);
262
+ const flag = firstBoolean([
263
+ r['hasInsurance'],
264
+ r['has_insurance'],
265
+ r['insured'],
266
+ r['isInsured'],
267
+ patient?.['hasInsurance'],
268
+ patient?.['has_insurance'],
269
+ patient?.['insured'],
270
+ nested?.['active'],
271
+ nested?.['isActive'],
272
+ ]);
273
+ if (flag !== null)
274
+ return { hasInsurance: flag, name };
275
+ const status = String(r['insuranceStatus'] ??
276
+ r['insurance_status'] ??
277
+ patient?.['insuranceStatus'] ??
278
+ patient?.['insurance_status'] ??
279
+ nested?.['status'] ??
280
+ (typeof r['insurance'] === 'string' ? r['insurance'] : '') ??
281
+ '')
282
+ .trim()
283
+ .toLowerCase();
284
+ if (status) {
285
+ if (INSURED_WORDS.includes(status))
286
+ return { hasInsurance: true, name };
287
+ if (UNINSURED_WORDS.includes(status))
288
+ return { hasInsurance: false, name };
289
+ }
290
+ // A payer name on its own implies coverage.
291
+ return { hasInsurance: name ? true : null, name };
292
+ }
293
+ function firstBoolean(values) {
294
+ for (const value of values) {
295
+ if (typeof value === 'boolean')
296
+ return value;
297
+ }
298
+ return null;
299
+ }
300
+ /** Pull visit type / slot / practice out of a messy free-text detail blob. */
301
+ function parseActivityDetail(detail) {
302
+ if (!detail)
303
+ return { visitType: null, startAt: null, practiceName: null };
304
+ const visitMatch = /\(([^)]+)\)\s*$/.exec(detail);
305
+ const visitType = visitMatch?.[1]?.trim() || null;
306
+ const dateMatch = /\bfor\s+(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}(?::\d{2})?))?/i.exec(detail) ??
307
+ /\b(\d{4}-\d{2}-\d{2})[ T](\d{1,2}:\d{2}(?::\d{2})?)/.exec(detail);
308
+ let startAt = null;
309
+ if (dateMatch) {
310
+ const date = dateMatch[1];
311
+ const time = dateMatch[2] ? dateMatch[2].padStart(5, '0') : '00:00';
312
+ startAt = `${date}T${time.length === 5 ? `${time}:00` : time}`;
313
+ }
314
+ const practiceMatch = /\bAT\s+([A-Z0-9][A-Z0-9 ,.'&-]{2,}?)(?:\s*\(|$)/.exec(detail);
315
+ const practiceName = practiceMatch?.[1]?.trim().replace(/,\s*$/, '') || null;
316
+ return { visitType, startAt, practiceName };
317
+ }
318
+ function activityTypeLabel(type) {
319
+ switch (type) {
320
+ case 'patient_created':
321
+ return 'Patient created';
322
+ case 'appointment_booked':
323
+ return 'Appointment booked';
324
+ case 'task_created':
325
+ return 'Task created';
326
+ default:
327
+ return type.replace(/_/g, ' ') || 'Activity';
328
+ }
329
+ }
330
+ function initialsFromName(name) {
331
+ if (!name?.trim())
332
+ return null;
333
+ const cleaned = name.replace(/^dr\.?\s+/i, '').trim();
334
+ const parts = cleaned.split(/\s+/).filter(Boolean);
335
+ if (!parts.length)
336
+ return null;
337
+ if (parts.length === 1)
338
+ return parts[0].slice(0, 2).toUpperCase();
339
+ return `${parts[0][0] ?? ''}${parts[parts.length - 1][0] ?? ''}`.toUpperCase();
340
+ }
341
+ function normalizeGender(value) {
342
+ const s = String(value ?? '').trim();
343
+ if (!s)
344
+ return null;
345
+ const key = s.toLowerCase();
346
+ if (key === 'm' || key === 'male')
347
+ return 'M';
348
+ if (key === 'f' || key === 'female')
349
+ return 'F';
350
+ if (key === 'o' || key === 'other' || key === 'non-binary' || key === 'nonbinary')
351
+ return 'O';
352
+ return s.length <= 3 ? s.toUpperCase() : s;
353
+ }
354
+ function normalizePriority(value) {
355
+ const key = String(value ?? '')
356
+ .trim()
357
+ .toLowerCase()
358
+ .replace(/[\s-]+/g, '_');
359
+ if (!key)
360
+ return null;
361
+ if (key === 'urgent' || key === 'critical')
362
+ return 'urgent';
363
+ if (key === 'high')
364
+ return 'high';
365
+ if (key === 'medium' || key === 'med' || key === 'normal')
366
+ return 'medium';
367
+ if (key === 'low')
368
+ return 'low';
369
+ return null;
370
+ }
371
+ function asRecord(value) {
372
+ return value && typeof value === 'object' && !Array.isArray(value)
373
+ ? value
374
+ : null;
375
+ }
376
+ function strOrNull(value) {
377
+ const s = String(value ?? '').trim();
378
+ return s || null;
379
+ }
380
+ function numOrNull(value) {
381
+ if (value == null || value === '')
382
+ return null;
383
+ const n = Number(value);
384
+ return Number.isFinite(n) ? n : null;
385
+ }
386
+
387
+ const DEFAULT_PAGE_SIZE = 50;
388
+ /** Parse `{ items, total, page, limit }` or a bare array (legacy). */
389
+ function asPaginatedResult(body, fallbackPage = 1, fallbackLimit = DEFAULT_PAGE_SIZE) {
390
+ if (Array.isArray(body)) {
391
+ return {
392
+ items: body,
393
+ total: body.length,
394
+ page: fallbackPage,
395
+ limit: fallbackLimit,
396
+ };
397
+ }
398
+ if (!body || typeof body !== 'object') {
399
+ return { items: [], total: 0, page: fallbackPage, limit: fallbackLimit };
400
+ }
401
+ const o = body;
402
+ const rawItems = o['items'] ?? o['data'];
403
+ const items = Array.isArray(rawItems) ? rawItems : [];
404
+ const total = typeof o['total'] === 'number' ? o['total'] : items.length;
405
+ const page = typeof o['page'] === 'number' ? o['page'] : fallbackPage;
406
+ const limit = typeof o['limit'] === 'number' ? o['limit'] : fallbackLimit;
407
+ return { items, total, page, limit };
408
+ }
409
+ function totalPages(total, pageSize) {
410
+ return Math.max(1, Math.ceil(total / pageSize));
411
+ }
412
+
413
+ const WELCOME_MESSAGE_MAX = 1000;
91
414
  function buildAutoWelcomeTemplate(practiceName, agentName) {
92
415
  const practice = practiceName.trim() || 'our office';
93
416
  const agent = agentName.trim() || 'your AI receptionist';
@@ -107,6 +430,112 @@ function computeEffectiveWelcomeMessage(settings) {
107
430
  return buildAutoWelcomeTemplate(String(general['practiceName'] ?? ''), String(general['agentName'] ?? ''));
108
431
  }
109
432
 
433
+ /** Call history table helpers — resolution badges + result chips. */
434
+ function historyResolution(input) {
435
+ if (input.risk === 'urgent' || input.risk === 'review')
436
+ return 'needs_attention';
437
+ const o = String(input.outcome ?? '').toLowerCase();
438
+ if (o.includes('fail') ||
439
+ o.includes('escalat') ||
440
+ o.includes('abandon') ||
441
+ o.includes('attention') ||
442
+ o.includes('voicemail') ||
443
+ o === 'transferred') {
444
+ return 'needs_attention';
445
+ }
446
+ return 'resolved';
447
+ }
448
+ function historyResolutionLabel(resolution) {
449
+ return resolution === 'resolved' ? 'RESOLVED' : 'NEEDS ATTENTION';
450
+ }
451
+ function resultChipIcon(label) {
452
+ const key = label.toLowerCase();
453
+ if (key.includes('appointment') || key.includes('book'))
454
+ return 'calendar_month';
455
+ if (key.includes('task'))
456
+ return 'check_circle';
457
+ if (key.includes('register') || key.includes('patient'))
458
+ return 'person_add';
459
+ if (key.includes('info'))
460
+ return 'info';
461
+ if (key.includes('transfer'))
462
+ return 'phone_forwarded';
463
+ if (key.includes('refill'))
464
+ return 'medication';
465
+ return 'label';
466
+ }
467
+ function callerMetaLine(input) {
468
+ if (isUnknownCaller(input.name, input.matched)) {
469
+ return String(input.phone ?? '').trim() || '—';
470
+ }
471
+ const age = input.age ?? ageFromDob(input.dob);
472
+ if (!input.dob && age == null && !input.gender) {
473
+ return String(input.phone ?? '').trim() || '—';
474
+ }
475
+ const parts = [];
476
+ if (input.dob) {
477
+ parts.push(`DOB: ${formatDobDisplay(input.dob)}`);
478
+ }
479
+ const ageGender = [];
480
+ if (age != null)
481
+ ageGender.push(`${age}Y`);
482
+ if (input.gender)
483
+ ageGender.push(String(input.gender).charAt(0).toUpperCase());
484
+ if (ageGender.length) {
485
+ parts.push(parts.length ? `• ${ageGender.join(' | ')}` : ageGender.join(' | '));
486
+ }
487
+ return parts.join(' ') || String(input.phone ?? '').trim() || '—';
488
+ }
489
+ function formatDobDisplay(dob) {
490
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(dob.trim());
491
+ if (m)
492
+ return `${m[2]}-${m[3]}-${m[1]}`;
493
+ return dob;
494
+ }
495
+ /** Age in whole years from YYYY-MM-DD (or partial ISO). */
496
+ function ageFromDob(dob, at = new Date()) {
497
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(dob ?? '').trim());
498
+ if (!m)
499
+ return null;
500
+ const y = Number(m[1]);
501
+ const mo = Number(m[2]);
502
+ const d = Number(m[3]);
503
+ if (!y || !mo || !d)
504
+ return null;
505
+ let age = at.getFullYear() - y;
506
+ const hadBirthday = at.getMonth() + 1 > mo || (at.getMonth() + 1 === mo && at.getDate() >= d);
507
+ if (!hadBirthday)
508
+ age -= 1;
509
+ return age >= 0 && age < 130 ? age : null;
510
+ }
511
+ function isUnknownCaller(name, matched) {
512
+ if (matched === false)
513
+ return true;
514
+ const n = String(name ?? '').trim();
515
+ return !n || /^unknown/i.test(n) || n === '?';
516
+ }
517
+ /** Derive result chips from explicit actions, else from outcome text. */
518
+ function historyResultChips(results, outcome) {
519
+ if (results?.length)
520
+ return results;
521
+ const o = outcome.trim();
522
+ if (!o)
523
+ return [];
524
+ const lower = o.toLowerCase();
525
+ if (lower.includes('appointment') || lower.includes('book'))
526
+ return ['Appointment Booked'];
527
+ if (lower.includes('task'))
528
+ return ['Task Created'];
529
+ if (lower.includes('register'))
530
+ return ['Patient Registered'];
531
+ if (lower.includes('info') || lower.includes('hours') || lower.includes('direction')) {
532
+ return ['Information Provided'];
533
+ }
534
+ if (lower === 'completed')
535
+ return ['Information Provided'];
536
+ return [o];
537
+ }
538
+
110
539
  /** V3 agent tool labels — mirrors GET /api/agent-tools/catalog */
111
540
  const AGENT_TOOL_LABELS = {
112
541
  find_doctors: 'Found Doctors',
@@ -213,6 +642,22 @@ function mapCallStatus(raw) {
213
642
  ];
214
643
  return allowed.includes(s) ? s : 'ai';
215
644
  }
645
+ function mapControlMode(raw, status, aiPaused) {
646
+ const mode = String(raw ?? '').toLowerCase().replace(/_/g, '-');
647
+ if (mode === 'ai' || mode === 'takeover' || mode === 'listen' || mode === 'paused') {
648
+ return mode;
649
+ }
650
+ if (aiPaused === true)
651
+ return 'paused';
652
+ const st = String(status ?? '').toLowerCase().replace(/_/g, '-');
653
+ if (st === 'takeover')
654
+ return 'takeover';
655
+ if (st === 'paused' || st === 'pause')
656
+ return 'paused';
657
+ if (st === 'listen' || st === 'listening')
658
+ return 'listen';
659
+ return 'ai';
660
+ }
216
661
  function mapSentiment(raw) {
217
662
  const s = String(raw ?? 'neutral').toLowerCase();
218
663
  if (s === 'calm' || s === 'neutral' || s === 'frustrated' || s === 'urgent')
@@ -257,10 +702,19 @@ function mapRecordingMeta(dto) {
257
702
  };
258
703
  }
259
704
  function mapLiveCallDto(dto) {
705
+ const dobRaw = String(dto.dob ?? dto.dateOfBirth ?? '').trim();
706
+ const dob = dobRaw ? dobRaw.slice(0, 10) : undefined;
707
+ const ageNum = typeof dto.age === 'number'
708
+ ? dto.age
709
+ : dto.age != null && String(dto.age).trim()
710
+ ? Number(dto.age)
711
+ : null;
712
+ const age = ageNum != null && Number.isFinite(ageNum) ? ageNum : ageFromDob(dob);
713
+ const gender = String(dto.gender ?? dto.sex ?? '').trim() || null;
260
714
  return {
261
715
  id: dto.id,
262
716
  name: dto.name,
263
- initials: dto.initials,
717
+ initials: dto.initials || (dto.matched ? '' : '?'),
264
718
  avatar: dto.avatar,
265
719
  phone: dto.phone,
266
720
  verification: mapVerification(dto.verification),
@@ -271,22 +725,48 @@ function mapLiveCallDto(dto) {
271
725
  sentiment: mapSentiment(dto.sentiment),
272
726
  confidence: normalizeConfidence(dto.confidence),
273
727
  goal: dto.goal,
274
- dob: dto.dob ?? undefined,
728
+ dob,
729
+ age,
730
+ gender,
275
731
  location: dto.location ?? '',
276
732
  provider: dto.provider,
277
733
  language: dto.language,
278
734
  matched: dto.matched,
279
735
  assigned: dto.assigned,
736
+ controlMode: mapControlMode(dto.controlMode, dto.status, dto.aiPaused),
280
737
  tools: (dto.tools ?? []).map(mapToolEvent),
281
738
  transcript: (dto.transcript ?? []).map(mapTranscriptLine),
739
+ aiPaused: dto.aiPaused === true || String(dto.controlMode ?? '').toLowerCase() === 'paused',
740
+ aiHandedOff: dto.aiHandedOff === true,
282
741
  };
283
742
  }
284
743
  function mapHistoryCallDto(dto) {
285
744
  const parsedStarted = Date.parse(dto.startedAt ?? dto.when);
745
+ const dobRaw = String(dto.dob ?? dto.dateOfBirth ?? '').trim();
746
+ const ageNum = typeof dto.age === 'number'
747
+ ? dto.age
748
+ : dto.age != null && String(dto.age).trim()
749
+ ? Number(dto.age)
750
+ : null;
751
+ const gender = String(dto.gender ?? dto.sex ?? '').trim() || null;
752
+ const handledRaw = String(dto.handledBy ?? dto.handler ?? '').trim().toLowerCase();
753
+ let handledBy;
754
+ if (handledRaw === 'staff' || handledRaw === 'human' || handledRaw === 'user') {
755
+ handledBy = 'staff';
756
+ }
757
+ else if (handledRaw === 'ai' || handledRaw === 'agent' || handledRaw === 'ava') {
758
+ handledBy = 'ai';
759
+ }
760
+ else if (String(dto.outcome ?? '').toLowerCase().includes('transfer')) {
761
+ handledBy = 'staff';
762
+ }
763
+ else {
764
+ handledBy = 'ai';
765
+ }
286
766
  return {
287
767
  id: dto.id,
288
768
  name: dto.name,
289
- initials: dto.initials,
769
+ initials: dto.initials || '?',
290
770
  phone: dto.phone,
291
771
  direction: dto.dir,
292
772
  outcome: dto.outcome,
@@ -298,6 +778,14 @@ function mapHistoryCallDto(dto) {
298
778
  location: dto.location,
299
779
  verification: mapVerification(dto.verify),
300
780
  review: dto.review === 'reviewed' ? 'reviewed' : 'needs',
781
+ handledBy,
782
+ dob: dobRaw ? dobRaw.slice(0, 10) : null,
783
+ age: ageNum != null && Number.isFinite(ageNum) ? ageNum : null,
784
+ gender,
785
+ results: Array.isArray(dto.actions)
786
+ ? dto.actions.map((a) => String(a).trim()).filter(Boolean)
787
+ : [],
788
+ risk: dto.risk ?? null,
301
789
  };
302
790
  }
303
791
  function mapHistoryDetailDto(call, detail, recordingUrl) {
@@ -492,6 +980,8 @@ function mapVolumeSeries(raw) {
492
980
  day: p.day,
493
981
  label: p.label,
494
982
  count: p.count,
983
+ inbound: p.inbound,
984
+ outbound: p.outbound,
495
985
  })),
496
986
  };
497
987
  }
@@ -569,22 +1059,77 @@ function mapSettingsResponse(body, safetyBody) {
569
1059
  status: c.ok ? 'ok' : 'warn',
570
1060
  }));
571
1061
  const mapped = {
572
- general: section('general'),
573
- phone: section('phoneAvailability'),
1062
+ general: normalizeGeneralSection(section('general')),
1063
+ phone: normalizePhoneSection(section('phoneAvailability')),
574
1064
  tasks: section('allowedTasks'),
575
1065
  scheduling: section('schedulingRules'),
576
1066
  refill: section('refillRules'),
577
1067
  verify: section('patientVerification'),
578
1068
  escalation: section('humanEscalation'),
579
1069
  outbound: section('outboundCalling'),
1070
+ knowledge: normalizeKnowledgeSection(section('knowledgeBase')),
580
1071
  safety,
581
- transcripts: section('transcriptsRecording'),
1072
+ transcripts: normalizeTranscriptsSection(section('transcriptsRecording')),
582
1073
  advanced: section('advanced'),
583
1074
  };
584
1075
  mapped.effectiveWelcomeMessage =
585
1076
  raw.effectiveWelcomeMessage ?? computeEffectiveWelcomeMessage(mapped);
1077
+ if (!mapped.knowledge) {
1078
+ mapped.knowledge = { whatToSay: '', whatNotToSay: '' };
1079
+ }
586
1080
  return mapped;
587
1081
  }
1082
+ function normalizeKnowledgeSection(raw) {
1083
+ return {
1084
+ ...raw,
1085
+ whatToSay: String(raw['whatToSay'] ?? raw['what_to_say'] ?? ''),
1086
+ whatNotToSay: String(raw['whatNotToSay'] ?? raw['what_not_to_say'] ?? ''),
1087
+ };
1088
+ }
1089
+ function normalizeGeneralSection(raw) {
1090
+ const language = String(raw['language'] ?? 'English');
1091
+ const languagesRaw = raw['languages'] ?? raw['languageList'] ?? language;
1092
+ const languages = Array.isArray(languagesRaw)
1093
+ ? languagesRaw.map(String).join(',')
1094
+ : String(languagesRaw || language);
1095
+ return {
1096
+ ...raw,
1097
+ languages,
1098
+ tone: String(raw['tone'] ?? 'professional'),
1099
+ };
1100
+ }
1101
+ function normalizePhoneSection(raw) {
1102
+ const locationIds = raw['locationIds'] ?? raw['location_ids'] ?? raw['locationsHandled'] ?? '';
1103
+ const locations = Array.isArray(locationIds) ? locationIds.map(String).join(',') : String(locationIds ?? '');
1104
+ const afterHoursBehavior = String(raw['afterHoursBehavior'] ??
1105
+ raw['after_hours_behavior'] ??
1106
+ (raw['afterHours'] === false ? 'voicemail' : 'ai_answers'));
1107
+ return {
1108
+ ...raw,
1109
+ answerMode: String(raw['answerMode'] ?? raw['answer_mode'] ?? 'always_first'),
1110
+ transferNumber: String(raw['transferNumber'] ?? raw['transfer_number'] ?? ''),
1111
+ ringTimeout: Number(raw['ringTimeout'] ?? raw['ring_timeout'] ?? 20) || 20,
1112
+ createFollowUpTask: Boolean(raw['createFollowUpTask'] ?? raw['create_follow_up_task'] ?? false),
1113
+ locationIds: locations,
1114
+ maxCallDurationMin: Number(raw['maxCallDurationMin'] ?? raw['max_call_duration_min'] ?? 5) || 5,
1115
+ silenceTimeoutSec: Number(raw['silenceTimeoutSec'] ?? raw['silence_timeout_sec'] ?? 30) || 30,
1116
+ afterHoursBehavior,
1117
+ };
1118
+ }
1119
+ function normalizeTranscriptsSection(raw) {
1120
+ const legacyRetain = Number(raw['retainDays'] ?? raw['retain_days'] ?? 30) || 30;
1121
+ return {
1122
+ ...raw,
1123
+ recordingRetainDays: Number(raw['recordingRetainDays'] ?? raw['recording_retain_days'] ?? legacyRetain) ||
1124
+ legacyRetain,
1125
+ transcriptRetainDays: Number(raw['transcriptRetainDays'] ?? raw['transcript_retain_days'] ?? legacyRetain) ||
1126
+ legacyRetain,
1127
+ disclosureWarningEnabled: Boolean(raw['disclosureWarningEnabled'] ?? raw['disclosure_warning_enabled'] ?? true),
1128
+ disclosureWarningText: String(raw['disclosureWarningText'] ??
1129
+ raw['disclosure_warning_text'] ??
1130
+ 'This call may be handled by an automated assistant and could be recorded for quality and training. You can ask to speak with a staff member at any time.'),
1131
+ };
1132
+ }
588
1133
  function humanizeSettingsKey(key) {
589
1134
  return key
590
1135
  .replace(/([A-Z])/g, ' $1')
@@ -603,6 +1148,7 @@ function mapSettingsToRequest(settings) {
603
1148
  patientVerification: settings.verify,
604
1149
  humanEscalation: settings.escalation,
605
1150
  outboundCalling: settings.outbound,
1151
+ knowledgeBase: settings.knowledge,
606
1152
  transcriptsRecording: settings.transcripts,
607
1153
  advanced: settings.advanced,
608
1154
  };
@@ -653,7 +1199,8 @@ function asTaskList(body) {
653
1199
  function asAuditList(body) {
654
1200
  if (Array.isArray(body))
655
1201
  return body;
656
- return [];
1202
+ const list = body;
1203
+ return list?.items ?? [];
657
1204
  }
658
1205
  function asAttentionList(body) {
659
1206
  if (Array.isArray(body))
@@ -661,7 +1208,7 @@ function asAttentionList(body) {
661
1208
  return [];
662
1209
  }
663
1210
 
664
- const now$1 = Date.now();
1211
+ const now$2 = Date.now();
665
1212
  function item(partial) {
666
1213
  return {
667
1214
  callId: 'LC-4821',
@@ -672,7 +1219,7 @@ function item(partial) {
672
1219
  durationMs: 340,
673
1220
  toolName: null,
674
1221
  errorMessage: null,
675
- startedAt: new Date(now$1 - 120_000).toISOString(),
1222
+ startedAt: new Date(now$2 - 120_000).toISOString(),
676
1223
  requestPreview: '{"phone":"+15551234567"}',
677
1224
  responsePreview: '{"items":[{"id":"p1"}]}',
678
1225
  ...partial,
@@ -688,7 +1235,7 @@ const SEED_CALL_API_LOGS = {
688
1235
  urlPath: '/vonage/answer',
689
1236
  statusCode: 200,
690
1237
  durationMs: 120,
691
- startedAt: new Date(now$1 - 300_000).toISOString(),
1238
+ startedAt: new Date(now$2 - 300_000).toISOString(),
692
1239
  requestPreview: '{"callId":"..."}',
693
1240
  responsePreview: '{"status":"connected"}',
694
1241
  }),
@@ -701,7 +1248,7 @@ const SEED_CALL_API_LOGS = {
701
1248
  urlPath: '/api/v1/patients',
702
1249
  statusCode: 200,
703
1250
  durationMs: 420,
704
- startedAt: new Date(now$1 - 240_000).toISOString(),
1251
+ startedAt: new Date(now$2 - 240_000).toISOString(),
705
1252
  }),
706
1253
  item({
707
1254
  id: 'log-3',
@@ -711,7 +1258,7 @@ const SEED_CALL_API_LOGS = {
711
1258
  urlPath: '/v1/chat/completions',
712
1259
  statusCode: 200,
713
1260
  durationMs: 890,
714
- startedAt: new Date(now$1 - 180_000).toISOString(),
1261
+ startedAt: new Date(now$2 - 180_000).toISOString(),
715
1262
  requestPreview: '{"model":"gpt-4o-mini"}',
716
1263
  responsePreview: '{"choices":[...]}',
717
1264
  }),
@@ -724,7 +1271,7 @@ const SEED_CALL_API_LOGS = {
724
1271
  urlPath: '/api/appointments',
725
1272
  statusCode: 201,
726
1273
  durationMs: 560,
727
- startedAt: new Date(now$1 - 60_000).toISOString(),
1274
+ startedAt: new Date(now$2 - 60_000).toISOString(),
728
1275
  }),
729
1276
  ],
730
1277
  'HC-9001': [
@@ -738,7 +1285,7 @@ const SEED_CALL_API_LOGS = {
738
1285
  success: false,
739
1286
  durationMs: 2100,
740
1287
  errorMessage: 'Internal Server Error',
741
- startedAt: new Date(now$1 - 86_400_000).toISOString(),
1288
+ startedAt: new Date(now$2 - 86_400_000).toISOString(),
742
1289
  }),
743
1290
  ],
744
1291
  };
@@ -749,7 +1296,7 @@ const SEED_API_LOG_DETAILS = {
749
1296
  requestBody: { patientId: 'p1', slotId: 'slot-9', reason: 'Annual checkup' },
750
1297
  responseBody: { appointmentId: 'apt-42', status: 'confirmed' },
751
1298
  metadata: { practiceId: 'prac-1', locationId: 'loc-1' },
752
- completedAt: new Date(now$1 - 59_500).toISOString(),
1299
+ completedAt: new Date(now$2 - 59_500).toISOString(),
753
1300
  },
754
1301
  'log-f1': {
755
1302
  ...SEED_CALL_API_LOGS['HC-9001'][0],
@@ -757,7 +1304,7 @@ const SEED_API_LOG_DETAILS = {
757
1304
  requestBody: { phone: '+15559876543' },
758
1305
  responseBody: { error: 'Internal Server Error', message: 'Database timeout' },
759
1306
  metadata: { retryCount: 1 },
760
- completedAt: new Date(now$1 - 86_398_000).toISOString(),
1307
+ completedAt: new Date(now$2 - 86_398_000).toISOString(),
761
1308
  },
762
1309
  };
763
1310
  function allSeedLogs() {
@@ -781,7 +1328,7 @@ const SEED_API_LOG_STATS = {
781
1328
  success: false,
782
1329
  durationMs: 2100,
783
1330
  errorMessage: 'Internal Server Error',
784
- startedAt: new Date(now$1 - 3_600_000).toISOString(),
1331
+ startedAt: new Date(now$2 - 3_600_000).toISOString(),
785
1332
  }),
786
1333
  item({
787
1334
  id: 'log-fail-2',
@@ -792,7 +1339,7 @@ const SEED_API_LOG_STATS = {
792
1339
  success: false,
793
1340
  durationMs: 1500,
794
1341
  errorMessage: 'Service unavailable',
795
- startedAt: new Date(now$1 - 7_200_000).toISOString(),
1342
+ startedAt: new Date(now$2 - 7_200_000).toISOString(),
796
1343
  }),
797
1344
  item({
798
1345
  id: 'log-fail-3',
@@ -803,13 +1350,13 @@ const SEED_API_LOG_STATS = {
803
1350
  success: false,
804
1351
  durationMs: 980,
805
1352
  errorMessage: 'Gateway timeout',
806
- startedAt: new Date(now$1 - 10_800_000).toISOString(),
1353
+ startedAt: new Date(now$2 - 10_800_000).toISOString(),
807
1354
  }),
808
1355
  ],
809
1356
  };
810
1357
 
811
- const now = Date.now();
812
- const min = (m) => now - m * 60_000;
1358
+ const now$1 = Date.now();
1359
+ const min = (m) => now$1 - m * 60_000;
813
1360
  const baseTranscript = [
814
1361
  { who: 'system', text: 'Call connected · inbound PSTN' },
815
1362
  {
@@ -848,6 +1395,8 @@ const SEED_LIVE_CALLS = [
848
1395
  confidence: 92,
849
1396
  goal: 'Reschedule visit with Dr. Chen',
850
1397
  dob: '1985-03-14',
1398
+ age: 41,
1399
+ gender: 'F',
851
1400
  location: 'Riverside Main',
852
1401
  provider: 'Dr. Elena Chen',
853
1402
  language: 'English',
@@ -895,6 +1444,9 @@ const SEED_LIVE_CALLS = [
895
1444
  sentiment: 'frustrated',
896
1445
  confidence: 71,
897
1446
  goal: 'Refill lisinopril 10mg',
1447
+ dob: '1978-09-22',
1448
+ age: 47,
1449
+ gender: 'M',
898
1450
  location: 'North Clinic',
899
1451
  provider: 'Dr. Patel',
900
1452
  language: 'English',
@@ -991,59 +1543,81 @@ const SEED_ATTENTION = [
991
1543
  const SEED_HISTORY = [
992
1544
  {
993
1545
  id: 'HC-9001',
994
- name: 'Linda Nguyen',
995
- initials: 'LN',
1546
+ name: 'Sooraj Jagadeesh',
1547
+ initials: 'SJ',
996
1548
  phone: '+1 (512) 555-0211',
997
1549
  direction: 'inbound',
998
1550
  outcome: 'Appointment booked',
999
- reason: 'New patient intake',
1000
- duration: 342,
1001
- started: min(45),
1551
+ reason: 'Appointment Refill',
1552
+ duration: 252,
1553
+ durationLabel: '4:12',
1554
+ started: Date.parse('2026-06-12T14:20:00'),
1002
1555
  location: 'Riverside Main',
1003
1556
  verification: 'verified',
1004
1557
  review: 'reviewed',
1558
+ handledBy: 'ai',
1559
+ dob: '2002-06-30',
1560
+ age: 23,
1561
+ gender: 'M',
1562
+ results: ['Appointment Booked'],
1005
1563
  },
1006
1564
  {
1007
1565
  id: 'HC-9002',
1008
- name: 'Robert Kim',
1009
- initials: 'RK',
1010
- phone: '+1 (737) 555-0333',
1566
+ name: 'Unknown Caller',
1567
+ initials: '?',
1568
+ phone: '+1 (555) 010-2299',
1011
1569
  direction: 'outbound',
1012
- outcome: 'Voicemail',
1013
- reason: 'Follow-up reminder',
1014
- duration: 28,
1015
- started: min(90),
1570
+ outcome: 'Task created',
1571
+ reason: 'Appointment',
1572
+ duration: 165,
1573
+ durationLabel: '2:45',
1574
+ started: Date.parse('2026-06-12T11:05:00'),
1016
1575
  location: 'North Clinic',
1017
- verification: 'verified',
1576
+ verification: 'unknown',
1018
1577
  review: 'needs',
1578
+ handledBy: 'ai',
1579
+ results: ['Task Created'],
1580
+ risk: 'review',
1019
1581
  },
1020
1582
  {
1021
1583
  id: 'HC-9003',
1022
- name: 'Sarah Williams',
1023
- initials: 'SW',
1584
+ name: 'Dinesh Kumar',
1585
+ initials: 'DK',
1024
1586
  phone: '+1 (512) 555-0444',
1025
1587
  direction: 'inbound',
1026
- outcome: 'Transferred',
1027
- reason: 'Insurance prior auth',
1028
- duration: 198,
1029
- started: min(120),
1588
+ outcome: 'Completed',
1589
+ reason: 'Appointment',
1590
+ duration: 372,
1591
+ durationLabel: '6:12',
1592
+ started: Date.parse('2026-06-11T16:45:00'),
1030
1593
  location: 'Riverside Main',
1031
1594
  verification: 'verified',
1032
- review: 'needs',
1595
+ review: 'reviewed',
1596
+ handledBy: 'staff',
1597
+ dob: '1988-03-14',
1598
+ age: 38,
1599
+ gender: 'M',
1600
+ results: ['Patient Registered', 'Appointment Booked'],
1033
1601
  },
1034
1602
  {
1035
1603
  id: 'HC-9004',
1036
- name: 'Carlos Mendez',
1037
- initials: 'CM',
1604
+ name: 'Meera Banerjee',
1605
+ initials: 'MB',
1038
1606
  phone: '+1 (512) 555-0555',
1039
1607
  direction: 'inbound',
1040
- outcome: 'Task created',
1041
- reason: 'Lab results question',
1042
- duration: 156,
1043
- started: min(180),
1608
+ outcome: 'Completed',
1609
+ reason: 'Information',
1610
+ duration: 93,
1611
+ durationLabel: '1:33',
1612
+ started: Date.parse('2026-06-11T09:12:00'),
1044
1613
  location: 'South Clinic',
1045
1614
  verification: 'partial',
1046
- review: 'reviewed',
1615
+ review: 'needs',
1616
+ handledBy: 'ai',
1617
+ dob: '1995-11-02',
1618
+ age: 30,
1619
+ gender: 'F',
1620
+ results: ['Information Provided'],
1047
1621
  },
1048
1622
  {
1049
1623
  id: 'HC-9005',
@@ -1054,10 +1628,13 @@ const SEED_HISTORY = [
1054
1628
  outcome: 'Completed',
1055
1629
  reason: 'Hours & directions',
1056
1630
  duration: 89,
1057
- started: min(240),
1631
+ durationLabel: '1:29',
1632
+ started: Date.parse('2026-06-10T13:40:00'),
1058
1633
  location: 'North Clinic',
1059
1634
  verification: 'unknown',
1060
1635
  review: 'reviewed',
1636
+ handledBy: 'ai',
1637
+ results: ['Information Provided'],
1061
1638
  },
1062
1639
  ];
1063
1640
  const SEED_HISTORY_DETAILS = {
@@ -1191,57 +1768,95 @@ const SEED_AUDIT = [
1191
1768
  const SEED_ANALYTICS = {
1192
1769
  range: '30d',
1193
1770
  kpiCards: [
1194
- { label: 'Calls handled', value: '1,247', delta: '+12%', tone: 'pos', sub: 'last 30 days' },
1195
- { label: 'AI resolution rate', value: '62%', delta: '+4%', tone: 'pos', sub: 'without transfer' },
1196
- { label: 'Avg handle time', value: '3:06', delta: '−8s', tone: 'pos', sub: 'vs prior period' },
1197
- { label: 'Transfers', value: '14%', delta: '−2%', tone: 'neutral', sub: 'patient requested human' },
1771
+ {
1772
+ label: 'Estimated Impact This Month',
1773
+ value: '96 hours',
1774
+ delta: '',
1775
+ tone: 'pos',
1776
+ sub: 'of front-desk time handled by the AI ~2.4 full work weeks.',
1777
+ },
1778
+ {
1779
+ label: 'AI Autonomous Resolution',
1780
+ value: '89%',
1781
+ delta: '+12%',
1782
+ tone: 'pos',
1783
+ sub: 'without staff intervention',
1784
+ },
1785
+ {
1786
+ label: 'Appointments Booked',
1787
+ value: '34',
1788
+ delta: '-35%',
1789
+ tone: 'warn',
1790
+ sub: 'Created by AI',
1791
+ },
1792
+ {
1793
+ label: 'Tasks Created',
1794
+ value: '19',
1795
+ delta: '-35%',
1796
+ tone: 'warn',
1797
+ sub: 'Created by AI',
1798
+ },
1799
+ {
1800
+ label: 'Patients Registered',
1801
+ value: '75',
1802
+ delta: '-6%',
1803
+ tone: 'warn',
1804
+ sub: 'Created by AI',
1805
+ },
1806
+ {
1807
+ label: 'Calls Handled',
1808
+ value: '1,234',
1809
+ delta: '+25%',
1810
+ tone: 'pos',
1811
+ sub: 'Inbound 104 · Outbound 24',
1812
+ },
1198
1813
  ],
1199
1814
  volume: {
1200
1815
  timezone: 'America/Chicago',
1201
- startDate: '2025-05-22',
1816
+ startDate: '2025-05-01',
1202
1817
  endDate: '2025-05-28',
1203
1818
  points: [
1204
- { date: '2025-05-22', day: 'Mon', label: 'Mon May 22', count: 42 },
1205
- { date: '2025-05-23', day: 'Tue', label: 'Tue May 23', count: 58 },
1206
- { date: '2025-05-24', day: 'Wed', label: 'Wed May 24', count: 51 },
1207
- { date: '2025-05-25', day: 'Thu', label: 'Thu May 25', count: 63 },
1208
- { date: '2025-05-26', day: 'Fri', label: 'Fri May 26', count: 47 },
1209
- { date: '2025-05-27', day: 'Sat', label: 'Sat May 27', count: 22 },
1210
- { date: '2025-05-28', day: 'Sun', label: 'Sun May 28', count: 18 },
1819
+ { date: '2025-05-01', day: 'Week 1', label: 'Week 1', count: 210, inbound: 170, outbound: 40 },
1820
+ { date: '2025-05-08', day: 'Week 2', label: 'Week 2', count: 255, inbound: 200, outbound: 55 },
1821
+ { date: '2025-05-15', day: 'Week 3', label: 'Week 3', count: 180, inbound: 145, outbound: 35 },
1822
+ { date: '2025-05-22', day: 'Week 4', label: 'Week 4', count: 290, inbound: 230, outbound: 60 },
1211
1823
  ],
1212
1824
  },
1213
1825
  outcomes: [
1214
- { label: 'Completed by AI', value: 62, color: '#3681F4' },
1215
- { label: 'Transferred', value: 18, color: '#8E51FF' },
1216
- { label: 'Voicemail', value: 8, color: '#71717A' },
1217
- { label: 'Abandoned', value: 12, color: '#F03740' },
1826
+ { label: 'AI handled', value: 78, color: '#8E51FF' },
1827
+ { label: 'Human Assisted', value: 18, color: '#3681F4' },
1828
+ { label: 'Needs Attention', value: 4, color: '#F472B6' },
1218
1829
  ],
1219
1830
  reasons: [
1220
- { label: 'Scheduling', value: 38 },
1221
- { label: 'Refills', value: 22 },
1222
- { label: 'Billing', value: 15 },
1223
- { label: 'Directions', value: 10 },
1224
- { label: 'Other', value: 15 },
1831
+ { label: 'Appointments', value: 68 },
1832
+ { label: 'Medication/Refills', value: 24 },
1833
+ { label: 'Message', value: 20 },
1834
+ { label: 'Information', value: 23 },
1835
+ { label: 'Registration', value: 5 },
1836
+ { label: 'Tasks', value: 20 },
1837
+ { label: 'Others', value: 9 },
1225
1838
  ],
1226
1839
  transfers: [
1227
- { label: 'Billing', value: 34 },
1228
- { label: 'Clinical', value: 28 },
1229
- { label: 'Insurance', value: 18 },
1230
- { label: 'Emergency', value: 5 },
1840
+ { label: 'Out of Scope', value: 68 },
1841
+ { label: 'Multiple Requests', value: 24 },
1842
+ { label: 'Identity Unresolved', value: 20 },
1843
+ { label: 'Others', value: 23 },
1231
1844
  ],
1232
1845
  peakHours: {
1233
1846
  timezone: 'America/Chicago',
1234
1847
  points: Array.from({ length: 24 }, (_, hour) => ({
1235
1848
  hour,
1236
1849
  label: hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`,
1237
- count: hour >= 8 && hour <= 17 ? 20 + Math.round(Math.sin((hour - 8) / 4) * 15) : 3 + (hour % 5),
1850
+ count: hour >= 8 && hour <= 20
1851
+ ? Math.round(30 + Math.sin((hour - 8) / 2.2) * 45 + (hour === 11 || hour === 16 ? 25 : 0))
1852
+ : 4 + (hour % 3),
1238
1853
  })),
1239
1854
  },
1240
1855
  kpis: {
1241
- totalCalls: 1247,
1856
+ totalCalls: 1234,
1242
1857
  avgDuration: 186,
1243
- bookingRate: 34,
1244
- transferRate: 14,
1858
+ bookingRate: 89,
1859
+ transferRate: 4,
1245
1860
  },
1246
1861
  };
1247
1862
  const SEED_SETTINGS = {
@@ -1249,26 +1864,50 @@ const SEED_SETTINGS = {
1249
1864
  practiceName: 'Riverside Family Medicine',
1250
1865
  timezone: 'America/Chicago',
1251
1866
  language: 'English',
1252
- agentName: 'Edvak AI',
1253
- welcomeMessage: 'Thank you for calling Riverside Family Medicine. This call may be recorded. How can I help you today?',
1867
+ languages: 'English,Spanish,Chinese,Hindi',
1868
+ agentName: 'Robin',
1869
+ tone: 'professional',
1870
+ welcomeMessage: 'Thanks for calling {{practiceName}}! This is {{agentName}}, the practice’s virtual assistant. How can I help you today?',
1254
1871
  ttsVoiceId: '7EzWGsX10sAS4c9m9cPf',
1255
1872
  },
1256
- phone: { mainNumber: '+1 (512) 555-0100', afterHours: true, ringTimeout: 30 },
1873
+ phone: {
1874
+ answerMode: 'always_first',
1875
+ mainNumber: '+1 (234) 092-0129',
1876
+ transferNumber: '+1 (234) 092-0129',
1877
+ ringTimeout: 20,
1878
+ createFollowUpTask: true,
1879
+ locationIds: 'loc-1,loc-2,loc-3',
1880
+ maxCallDurationMin: 5,
1881
+ silenceTimeoutSec: 30,
1882
+ afterHoursBehavior: 'ai_answers',
1883
+ afterHours: true,
1884
+ },
1257
1885
  tasks: { autoCreate: true, defaultAssignee: 'Front desk' },
1258
1886
  scheduling: { leadTimeHours: 4, allowSameDay: true },
1259
1887
  refill: { autoApproveDays: 7, requireStaffReview: true },
1260
1888
  verify: { requireDob: true, requireName: true, maxAttempts: 3 },
1261
1889
  escalation: { sentimentThreshold: 'frustrated', urgentKeywords: 'emergency, chest pain' },
1262
1890
  outbound: { enabled: true, callerId: '+1 (512) 555-0100' },
1891
+ knowledge: {
1892
+ whatToSay: 'Arrive 15 minutes early for new-patient visits. Bring a photo ID and insurance card. We accept most major plans and self-pay; ask front desk for current cash-pay rates. For medical emergencies, hang up and dial 911.',
1893
+ whatNotToSay: 'Do not give clinical advice, diagnose symptoms, or promise specific appointment times that are not available. Do not share other patients’ information. Do not quote prices that are not listed in practice guidance.',
1894
+ },
1263
1895
  safety: [
1264
1896
  { id: 's1', label: 'HIPAA logging enabled', status: 'ok' },
1265
1897
  { id: 's2', label: 'Recording consent script', status: 'ok' },
1266
1898
  { id: 's3', label: 'Emergency routing tested', status: 'warn' },
1267
1899
  { id: 's4', label: 'PHI redaction in transcripts', status: 'ok' },
1268
1900
  ],
1269
- transcripts: { retainDays: 90, redactPhi: true },
1901
+ transcripts: {
1902
+ recordingRetainDays: 30,
1903
+ transcriptRetainDays: 30,
1904
+ disclosureWarningEnabled: true,
1905
+ disclosureWarningText: 'This call may be handled by an automated assistant and could be recorded for quality and training. You can ask to speak with a staff member at any time.',
1906
+ retainDays: 30,
1907
+ redactPhi: true,
1908
+ },
1270
1909
  advanced: { modelVersion: 'edvak-v2', debugMode: false },
1271
- effectiveWelcomeMessage: 'Thank you for calling Riverside Family Medicine. This call may be recorded. How can I help you today?',
1910
+ effectiveWelcomeMessage: 'Thanks for calling Riverside Family Medicine! This is Robin, the practice’s virtual assistant. How can I help you today?',
1272
1911
  };
1273
1912
  const SEED_SETUP = {
1274
1913
  readiness: 92,
@@ -1396,6 +2035,210 @@ const SEED_OUTBOUND_PURPOSES = [
1396
2035
  },
1397
2036
  ];
1398
2037
 
2038
+ const hour = 60 * 60 * 1000;
2039
+ const now = Date.now();
2040
+ function todayAt(h, m = 0) {
2041
+ const d = new Date();
2042
+ d.setHours(h, m, 0, 0);
2043
+ return d.toISOString();
2044
+ }
2045
+ const SEED_ACTIVITIES = [
2046
+ {
2047
+ id: 'act-appt-1',
2048
+ type: 'appointment_booked',
2049
+ occurredAt: new Date(now - 0.4 * hour).toISOString(),
2050
+ title: 'Appointment booked',
2051
+ detail: '',
2052
+ patientId: 'pat-1001',
2053
+ patientName: 'Sooraj Jagadeesh',
2054
+ patientInitials: 'SJ',
2055
+ patientDob: '2002-06-30',
2056
+ patientAge: 23,
2057
+ patientGender: 'M',
2058
+ providerName: 'Dr. Anika Mehra',
2059
+ providerInitials: 'AM',
2060
+ providerSpecialty: 'Cardiologist',
2061
+ appointmentId: 'appt-9001',
2062
+ callId: 'LC-4801',
2063
+ startAt: todayAt(8, 0),
2064
+ endAt: todayAt(8, 15),
2065
+ durationMinutes: 15,
2066
+ visitType: 'Follow Up',
2067
+ hasInsurance: true,
2068
+ insuranceName: 'Aetna PPO',
2069
+ },
2070
+ {
2071
+ id: 'act-appt-1b',
2072
+ type: 'task_created',
2073
+ occurredAt: new Date(now - 0.35 * hour).toISOString(),
2074
+ title: 'Needs Scheduling Assistance',
2075
+ detail: '',
2076
+ patientId: 'pat-1001',
2077
+ patientName: 'Sooraj Jagadeesh',
2078
+ patientInitials: 'SJ',
2079
+ patientDob: '2002-06-30',
2080
+ patientAge: 23,
2081
+ patientGender: 'M',
2082
+ providerName: 'Front Desk',
2083
+ providerInitials: 'FD',
2084
+ providerSpecialty: 'Operations',
2085
+ taskId: 'task-4801',
2086
+ callId: 'LC-4801',
2087
+ startAt: todayAt(8, 5),
2088
+ visitType: 'Insurance verification',
2089
+ durationMinutes: 10,
2090
+ priority: 'medium',
2091
+ },
2092
+ {
2093
+ id: 'act-appt-2',
2094
+ type: 'appointment_booked',
2095
+ occurredAt: new Date(now - 1.1 * hour).toISOString(),
2096
+ title: 'Appointment booked',
2097
+ detail: '',
2098
+ patientId: 'pat-1005',
2099
+ patientName: 'Lisa Sullivan',
2100
+ patientInitials: 'LS',
2101
+ patientDob: '1998-03-12',
2102
+ patientAge: 28,
2103
+ patientGender: 'F',
2104
+ providerName: 'Dr. Marcus Webb',
2105
+ providerInitials: 'MW',
2106
+ providerSpecialty: 'Endocrinologist',
2107
+ appointmentId: 'appt-9003',
2108
+ callId: 'LC-4800',
2109
+ startAt: todayAt(8, 15),
2110
+ endAt: todayAt(8, 30),
2111
+ durationMinutes: 15,
2112
+ visitType: 'Lab Results Review',
2113
+ hasInsurance: false,
2114
+ },
2115
+ {
2116
+ id: 'act-appt-3',
2117
+ type: 'appointment_booked',
2118
+ occurredAt: new Date(now - 2.2 * hour).toISOString(),
2119
+ title: 'Appointment booked',
2120
+ detail: '',
2121
+ patientId: 'pat-1006',
2122
+ patientName: 'Aiden Rivera',
2123
+ patientInitials: 'AR',
2124
+ patientDob: '1995-11-04',
2125
+ patientAge: 30,
2126
+ patientGender: 'M',
2127
+ providerName: 'Dr. Priya Nair',
2128
+ providerInitials: 'PN',
2129
+ providerSpecialty: 'General Practitioner',
2130
+ appointmentId: 'appt-9004',
2131
+ callId: 'LC-4799',
2132
+ startAt: todayAt(8, 30),
2133
+ endAt: todayAt(8, 45),
2134
+ durationMinutes: 15,
2135
+ visitType: 'Routine Checkup',
2136
+ },
2137
+ {
2138
+ id: 'act-appt-4',
2139
+ type: 'appointment_booked',
2140
+ occurredAt: new Date(now - 8 * hour).toISOString(),
2141
+ title: 'Appointment booked',
2142
+ detail: '',
2143
+ patientId: 'pat-1003',
2144
+ patientName: 'Aisha Patel',
2145
+ patientInitials: 'AP',
2146
+ patientDob: '1988-01-19',
2147
+ patientAge: 38,
2148
+ patientGender: 'F',
2149
+ providerName: 'Dr. Jordan Lee',
2150
+ providerInitials: 'JL',
2151
+ providerSpecialty: 'Internal Medicine',
2152
+ appointmentId: 'appt-9002',
2153
+ callId: 'LC-4790',
2154
+ startAt: todayAt(14, 30),
2155
+ endAt: todayAt(15, 0),
2156
+ durationMinutes: 30,
2157
+ visitType: 'Annual Wellness',
2158
+ hasInsurance: true,
2159
+ insuranceName: 'BlueCross Blue Shield',
2160
+ },
2161
+ {
2162
+ id: 'act-1',
2163
+ type: 'patient_created',
2164
+ occurredAt: new Date(now - 0.5 * hour).toISOString(),
2165
+ title: 'Patient registered',
2166
+ detail: '',
2167
+ patientId: 'pat-1008',
2168
+ patientName: 'Maria Garcia',
2169
+ patientInitials: 'MG',
2170
+ patientDob: '1991-04-22',
2171
+ patientAge: 35,
2172
+ patientGender: 'F',
2173
+ callId: 'LC-4801',
2174
+ startAt: todayAt(9, 12),
2175
+ visitType: 'New registration',
2176
+ hasInsurance: true,
2177
+ insuranceName: 'UnitedHealthcare',
2178
+ },
2179
+ {
2180
+ id: 'act-5',
2181
+ type: 'patient_created',
2182
+ occurredAt: new Date(now - 26 * hour).toISOString(),
2183
+ title: 'Patient registered',
2184
+ detail: '',
2185
+ patientId: 'pat-1004',
2186
+ patientName: 'Sam Okonkwo',
2187
+ patientInitials: 'SO',
2188
+ patientDob: '1979-09-08',
2189
+ patientAge: 46,
2190
+ patientGender: 'M',
2191
+ callId: 'LC-4782',
2192
+ startAt: new Date(now - 26 * hour).toISOString(),
2193
+ visitType: 'Outbound registration',
2194
+ hasInsurance: false,
2195
+ },
2196
+ {
2197
+ id: 'act-3',
2198
+ type: 'task_created',
2199
+ occurredAt: new Date(now - 3 * hour).toISOString(),
2200
+ title: 'Needs Scheduling Assistance',
2201
+ detail: '',
2202
+ patientId: 'pat-1002',
2203
+ patientName: 'James Chen',
2204
+ patientInitials: 'JC',
2205
+ patientDob: '1985-12-01',
2206
+ patientAge: 40,
2207
+ patientGender: 'M',
2208
+ providerName: 'Front Desk',
2209
+ providerInitials: 'FD',
2210
+ providerSpecialty: 'Operations',
2211
+ taskId: 'task-301',
2212
+ callId: 'LC-4798',
2213
+ startAt: new Date(now - 3 * hour).toISOString(),
2214
+ visitType: 'Insurance verification',
2215
+ durationMinutes: 15,
2216
+ priority: 'medium',
2217
+ },
2218
+ {
2219
+ id: 'act-6',
2220
+ type: 'task_created',
2221
+ occurredAt: new Date(now - 5 * hour).toISOString(),
2222
+ title: 'Needs Scheduling Assistance',
2223
+ detail: '',
2224
+ patientId: 'pat-1007',
2225
+ patientName: 'Elena Foster',
2226
+ patientInitials: 'EF',
2227
+ patientDob: '1993-07-15',
2228
+ patientAge: 32,
2229
+ patientGender: 'F',
2230
+ providerName: 'Nurse Station',
2231
+ providerInitials: 'NS',
2232
+ providerSpecialty: 'Clinical support',
2233
+ taskId: 'task-302',
2234
+ callId: 'LC-4795',
2235
+ startAt: new Date(now - 5 * hour).toISOString(),
2236
+ visitType: 'Clinical callback',
2237
+ durationMinutes: 10,
2238
+ priority: 'medium',
2239
+ },
2240
+ ];
2241
+
1399
2242
  const MOCK_DELAY_MS = 200;
1400
2243
  class AiReceptionistApiMockService {
1401
2244
  constructor() {
@@ -1413,6 +2256,17 @@ class AiReceptionistApiMockService {
1413
2256
  mock(data) {
1414
2257
  return of({ data }).pipe(delay(MOCK_DELAY_MS));
1415
2258
  }
2259
+ paginate(rows, page = 1, limit = DEFAULT_PAGE_SIZE) {
2260
+ const safePage = Math.max(1, page);
2261
+ const safeLimit = Math.min(100, Math.max(1, limit));
2262
+ const start = (safePage - 1) * safeLimit;
2263
+ return of({
2264
+ items: rows.slice(start, start + safeLimit),
2265
+ total: rows.length,
2266
+ page: safePage,
2267
+ limit: safeLimit,
2268
+ }).pipe(delay(MOCK_DELAY_MS));
2269
+ }
1416
2270
  getStatus() {
1417
2271
  return this.mock(this.status());
1418
2272
  }
@@ -1495,7 +2349,7 @@ class AiReceptionistApiMockService {
1495
2349
  rows = rows.filter((h) => h.location === filters.location);
1496
2350
  if (filters?.review)
1497
2351
  rows = rows.filter((h) => h.review === filters.review);
1498
- return this.mock(rows);
2352
+ return this.paginate(rows, filters?.page, filters?.limit);
1499
2353
  }
1500
2354
  getCallDetail(id) {
1501
2355
  const detail = SEED_HISTORY_DETAILS[id] ?? null;
@@ -1522,17 +2376,58 @@ class AiReceptionistApiMockService {
1522
2376
  });
1523
2377
  }
1524
2378
  postTakeover(id) {
1525
- this.patchLive(id, (c) => ({ ...c, status: 'takeover', assigned: 'You' }));
2379
+ return this.postMediaJoin(id, 'takeover').pipe(map(() => ({ data: { success: true } })));
2380
+ }
2381
+ postMediaJoin(id, mode) {
2382
+ this.patchLive(id, (c) => ({
2383
+ ...c,
2384
+ status: mode === 'takeover' ? 'takeover' : c.status,
2385
+ controlMode: mode,
2386
+ assigned: mode === 'takeover' ? 'You' : c.assigned,
2387
+ aiPaused: mode === 'takeover',
2388
+ }));
2389
+ // Mock tokens are placeholders — StaffMediaService is skipped when useMockApi is on.
2390
+ return this.mock({
2391
+ ok: true,
2392
+ callId: id,
2393
+ mode,
2394
+ provider: 'livekit',
2395
+ livekitUrl: 'wss://mock.livekit.local',
2396
+ clientToken: `mock-token-${mode}-${id}`,
2397
+ roomName: `call-${id}`,
2398
+ controlMode: mode,
2399
+ aiPaused: mode === 'takeover',
2400
+ assigned: mode === 'takeover' ? 'You' : null,
2401
+ });
2402
+ }
2403
+ postMediaLeave(id) {
2404
+ this.patchLive(id, (c) => ({
2405
+ ...c,
2406
+ status: 'ai',
2407
+ controlMode: 'ai',
2408
+ assigned: null,
2409
+ aiPaused: false,
2410
+ }));
1526
2411
  return this.mock({ success: true });
1527
2412
  }
1528
2413
  postResumeAi(id) {
1529
- this.patchLive(id, (c) => ({ ...c, status: 'ai', assigned: null, aiPaused: false }));
1530
- return this.mock({ success: true });
2414
+ return this.postMediaLeave(id);
1531
2415
  }
1532
2416
  postPause(id) {
1533
- this.patchLive(id, (c) => ({ ...c, aiPaused: true }));
2417
+ this.patchLive(id, (c) => ({ ...c, controlMode: 'paused', aiPaused: true }));
2418
+ return this.mock({ success: true });
2419
+ }
2420
+ postResume(id) {
2421
+ this.patchLive(id, (c) => ({
2422
+ ...c,
2423
+ controlMode: c.status === 'takeover' || c.assigned ? 'takeover' : 'ai',
2424
+ aiPaused: false,
2425
+ }));
1534
2426
  return this.mock({ success: true });
1535
2427
  }
2428
+ postListen(id) {
2429
+ return this.postMediaJoin(id, 'listen').pipe(map(() => ({ data: { success: true } })));
2430
+ }
1536
2431
  postTransfer(id) {
1537
2432
  this.patchLive(id, (c) => ({ ...c, status: 'transferring' }));
1538
2433
  return this.mock({ success: true });
@@ -1566,10 +2461,10 @@ class AiReceptionistApiMockService {
1566
2461
  rows = rows.filter((t) => t.source === filters.source);
1567
2462
  if (filters?.callId)
1568
2463
  rows = rows.filter((t) => t.callId === filters.callId);
1569
- return this.mock(rows);
2464
+ return this.paginate(rows, filters?.page, filters?.limit);
1570
2465
  }
1571
2466
  getCallTasks(callId) {
1572
- return this.getTasks({ callId });
2467
+ return this.getTasks({ callId, limit: 100 }).pipe(map((page) => ({ data: page.items })));
1573
2468
  }
1574
2469
  createTask(req) {
1575
2470
  let description = req.description?.trim() ?? '';
@@ -1623,13 +2518,22 @@ class AiReceptionistApiMockService {
1623
2518
  ]);
1624
2519
  return of(new Blob([silent], { type: 'audio/wav' })).pipe(delay(MOCK_DELAY_MS));
1625
2520
  }
2521
+ testKnowledgeAnswer(params) {
2522
+ const answer = `Based on our guidance: for “${params.question.trim()}”, ` +
2523
+ `I'd share what's in our practice knowledge and avoid restricted topics.`;
2524
+ return this.previewVoice(params.voiceId ?? 'mock').pipe(map((audioBlob) => ({
2525
+ answer,
2526
+ voiceId: params.voiceId ?? 'mock',
2527
+ audioBlob,
2528
+ })));
2529
+ }
1626
2530
  withEffectiveWelcome(settings) {
1627
2531
  return {
1628
2532
  ...settings,
1629
2533
  effectiveWelcomeMessage: computeEffectiveWelcomeMessage(settings),
1630
2534
  };
1631
2535
  }
1632
- getAudit(q, actor) {
2536
+ getAudit(q, actor, opts) {
1633
2537
  let rows = SEED_AUDIT;
1634
2538
  if (q) {
1635
2539
  const needle = q.toLowerCase();
@@ -1639,7 +2543,30 @@ class AiReceptionistApiMockService {
1639
2543
  }
1640
2544
  if (actor)
1641
2545
  rows = rows.filter((e) => e.actor.toLowerCase().includes(actor.toLowerCase()));
1642
- return this.mock(rows);
2546
+ return this.paginate(rows, opts?.page, opts?.limit);
2547
+ }
2548
+ getActivities(filters) {
2549
+ let rows = structuredClone(SEED_ACTIVITIES);
2550
+ const types = (filters?.types ?? '')
2551
+ .split(',')
2552
+ .map((t) => t.trim())
2553
+ .filter(Boolean);
2554
+ if (types.length) {
2555
+ rows = rows.filter((a) => types.includes(a.type));
2556
+ }
2557
+ if (filters?.callId?.trim()) {
2558
+ const callId = filters.callId.trim().toLowerCase();
2559
+ rows = rows.filter((a) => (a.callId || '').toLowerCase() === callId);
2560
+ }
2561
+ if (filters?.search?.trim()) {
2562
+ const needle = filters.search.trim().toLowerCase();
2563
+ rows = rows.filter((a) => [a.title, a.detail, a.patientName, a.callId, a.type]
2564
+ .filter(Boolean)
2565
+ .join(' ')
2566
+ .toLowerCase()
2567
+ .includes(needle));
2568
+ }
2569
+ return this.paginate(rows, filters?.page, filters?.limit);
1643
2570
  }
1644
2571
  getSetup() {
1645
2572
  return this.mock(SEED_SETUP);
@@ -1796,12 +2723,78 @@ class AiReceptionistApiMockService {
1796
2723
  return of({ success: true }).pipe(delay(MOCK_DELAY_MS));
1797
2724
  }
1798
2725
  placeOutboundCall(body) {
2726
+ const idHint = body.appointmentId
2727
+ ? ` (appt ${body.appointmentId})`
2728
+ : body.patientId
2729
+ ? ` (patient ${body.patientId})`
2730
+ : '';
1799
2731
  return of({
1800
2732
  success: true,
1801
2733
  callId: `LC-${Math.floor(4800 + Math.random() * 100)}`,
1802
- message: `Outbound call started to ${body.phone}`,
2734
+ message: `Outbound call started to ${body.phone}${idHint}`,
1803
2735
  }).pipe(delay(MOCK_DELAY_MS));
1804
2736
  }
2737
+ searchPatientsForOutbound(q, limit = 10) {
2738
+ const needle = q.trim().toLowerCase();
2739
+ const digits = q.replace(/\D/g, '');
2740
+ const hits = [
2741
+ {
2742
+ patientId: 'pat-1001',
2743
+ firstName: 'Maria',
2744
+ lastName: 'Garcia',
2745
+ fullName: 'Maria Garcia',
2746
+ phone: '+14155550100',
2747
+ dob: '1988-04-12',
2748
+ },
2749
+ {
2750
+ patientId: 'pat-1002',
2751
+ firstName: 'James',
2752
+ lastName: 'Chen',
2753
+ fullName: 'James Chen',
2754
+ phone: '+14155550122',
2755
+ dob: '1975-11-03',
2756
+ },
2757
+ {
2758
+ patientId: 'pat-1003',
2759
+ firstName: 'Aisha',
2760
+ lastName: 'Patel',
2761
+ fullName: 'Aisha Patel',
2762
+ phone: '+14155550987',
2763
+ dob: '1992-07-21',
2764
+ },
2765
+ ].filter((p) => {
2766
+ if (!needle)
2767
+ return false;
2768
+ const hay = `${p.fullName} ${p.phone} ${p.patientId}`.toLowerCase();
2769
+ return hay.includes(needle) || (!!digits && (p.phone ?? '').includes(digits));
2770
+ });
2771
+ return of(hits.slice(0, limit)).pipe(delay(MOCK_DELAY_MS));
2772
+ }
2773
+ listPatientAppointmentsForOutbound(patientId, limit = 20) {
2774
+ const now = Date.now();
2775
+ const day = 24 * 60 * 60 * 1000;
2776
+ const all = [
2777
+ {
2778
+ appointmentId: `appt-${patientId}-1`,
2779
+ startTime: new Date(now + 2 * day).toISOString(),
2780
+ endTime: new Date(now + 2 * day + 30 * 60 * 1000).toISOString(),
2781
+ providerName: 'Dr. Patel',
2782
+ locationName: 'Main Clinic',
2783
+ status: 'scheduled',
2784
+ visitType: 'Follow-up',
2785
+ },
2786
+ {
2787
+ appointmentId: `appt-${patientId}-2`,
2788
+ startTime: new Date(now + 9 * day).toISOString(),
2789
+ endTime: new Date(now + 9 * day + 45 * 60 * 1000).toISOString(),
2790
+ providerName: 'Dr. Nguyen',
2791
+ locationName: 'West Wing',
2792
+ status: 'confirmed',
2793
+ visitType: 'Annual wellness',
2794
+ },
2795
+ ];
2796
+ return of(all.slice(0, limit)).pipe(delay(MOCK_DELAY_MS));
2797
+ }
1805
2798
  patchLive(id, fn) {
1806
2799
  this.liveCalls.update((calls) => calls.map((c) => (c.id === id ? fn(structuredClone(c)) : c)));
1807
2800
  }
@@ -1937,7 +2930,9 @@ class AiReceptionistApiService {
1937
2930
  getHistory(filters) {
1938
2931
  if (this.useMock)
1939
2932
  return this.mock.getHistory(filters);
1940
- let params = new HttpParams().set('page', '1').set('limit', '100');
2933
+ const page = filters?.page ?? 1;
2934
+ const limit = filters?.limit ?? DEFAULT_PAGE_SIZE;
2935
+ let params = new HttpParams().set('page', String(page)).set('limit', String(limit));
1941
2936
  if (filters?.q)
1942
2937
  params = params.set('search', filters.q);
1943
2938
  if (filters?.dir)
@@ -1948,7 +2943,13 @@ class AiReceptionistApiService {
1948
2943
  params = params.set('location', filters.location);
1949
2944
  if (filters?.review)
1950
2945
  params = params.set('review', filters.review);
1951
- return this.http.get(`${this.arBase}/calls/history`, { params }).pipe(map((body) => ({ data: asHistoryList(body).map(mapHistoryCallDto) })));
2946
+ return this.http.get(`${this.arBase}/calls/history`, { params }).pipe(map((body) => {
2947
+ const pageResult = asPaginatedResult(body, page, limit);
2948
+ return {
2949
+ ...pageResult,
2950
+ items: pageResult.items.map(mapHistoryCallDto),
2951
+ };
2952
+ }));
1952
2953
  }
1953
2954
  getCallDetail(id) {
1954
2955
  if (this.useMock)
@@ -1979,10 +2980,49 @@ class AiReceptionistApiService {
1979
2980
  status: err.status,
1980
2981
  }))));
1981
2982
  }
2983
+ /**
2984
+ * Staff softphone join into LiveKit room `call-{callId}`.
2985
+ * POST /calls/:id/media/join { mode: listen|takeover }
2986
+ */
2987
+ postMediaJoin(id, mode) {
2988
+ if (this.useMock)
2989
+ return this.mock.postMediaJoin(id, mode);
2990
+ const enc = encodeURIComponent(id);
2991
+ return this.http
2992
+ .post(`${this.arBase}/calls/${enc}/media/join`, { mode })
2993
+ .pipe(map((body) => {
2994
+ const data = mapStaffMediaJoinResult(body);
2995
+ if (!data) {
2996
+ throw new Error('media/join response missing livekitUrl or clientToken');
2997
+ }
2998
+ return { data: { ...data, callId: data.callId || id, mode } };
2999
+ }), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to join call media')))));
3000
+ }
3001
+ /** Drop staff WebRTC only; does not hang up the patient. */
3002
+ postMediaLeave(id) {
3003
+ if (this.useMock)
3004
+ return this.mock.postMediaLeave(id);
3005
+ const enc = encodeURIComponent(id);
3006
+ return this.http
3007
+ .post(`${this.arBase}/calls/${enc}/media/leave`, {})
3008
+ .pipe(map(() => ({ data: { success: true } })));
3009
+ }
3010
+ /**
3011
+ * Take over — same join payload as media/join { mode: takeover }.
3012
+ * Prefer this or postMediaJoin('takeover'); both are supported by BE.
3013
+ */
1982
3014
  postTakeover(id) {
1983
3015
  if (this.useMock)
1984
- return this.mock.postTakeover(id);
1985
- return this.postCallAction(id, 'takeover');
3016
+ return this.mock.postMediaJoin(id, 'takeover');
3017
+ const enc = encodeURIComponent(id);
3018
+ return this.http.post(`${this.arBase}/calls/${enc}/takeover`, {}).pipe(map((body) => {
3019
+ const data = mapStaffMediaJoinResult(body);
3020
+ if (!data) {
3021
+ throw new Error('takeover response missing livekitUrl or clientToken');
3022
+ }
3023
+ const mode = 'takeover';
3024
+ return { data: { ...data, callId: data.callId || id, mode } };
3025
+ }), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to take over call')))));
1986
3026
  }
1987
3027
  postResumeAi(id) {
1988
3028
  if (this.useMock)
@@ -1994,6 +3034,25 @@ class AiReceptionistApiService {
1994
3034
  return this.mock.postPause(id);
1995
3035
  return this.postCallAction(id, 'pause');
1996
3036
  }
3037
+ postResume(id) {
3038
+ if (this.useMock)
3039
+ return this.mock.postResume(id);
3040
+ return this.postCallAction(id, 'resume');
3041
+ }
3042
+ /** Listen — same join payload as media/join { mode: listen }. */
3043
+ postListen(id) {
3044
+ if (this.useMock)
3045
+ return this.mock.postMediaJoin(id, 'listen');
3046
+ const enc = encodeURIComponent(id);
3047
+ return this.http.post(`${this.arBase}/calls/${enc}/listen`, {}).pipe(map((body) => {
3048
+ const data = mapStaffMediaJoinResult(body);
3049
+ if (!data) {
3050
+ throw new Error('listen response missing livekitUrl or clientToken');
3051
+ }
3052
+ const mode = 'listen';
3053
+ return { data: { ...data, callId: data.callId || id, mode } };
3054
+ }), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to listen to call')))));
3055
+ }
1997
3056
  postTransfer(id) {
1998
3057
  if (this.useMock)
1999
3058
  return this.mock.postTransfer(id);
@@ -2026,7 +3085,9 @@ class AiReceptionistApiService {
2026
3085
  getTasks(filters) {
2027
3086
  if (this.useMock)
2028
3087
  return this.mock.getTasks(filters);
2029
- let params = new HttpParams();
3088
+ const page = filters?.page ?? 1;
3089
+ const limit = filters?.limit ?? DEFAULT_PAGE_SIZE;
3090
+ let params = new HttpParams().set('page', String(page)).set('limit', String(limit));
2030
3091
  if (filters?.status) {
2031
3092
  const apiStatus = filters.status === 'in_progress' ? 'in-progress' : filters.status;
2032
3093
  params = params.set('status', apiStatus);
@@ -2037,9 +3098,13 @@ class AiReceptionistApiService {
2037
3098
  params = params.set('source', filters.source);
2038
3099
  if (filters?.callId)
2039
3100
  params = params.set('callId', filters.callId);
2040
- if (filters?.limit != null)
2041
- params = params.set('limit', String(filters.limit));
2042
- return this.http.get(`${this.arBase}/tasks`, { params }).pipe(map((body) => ({ data: asTaskList(body).map(mapTaskDto) })));
3101
+ return this.http.get(`${this.arBase}/tasks`, { params }).pipe(map((body) => {
3102
+ const pageResult = asPaginatedResult(body, page, limit);
3103
+ return {
3104
+ ...pageResult,
3105
+ items: pageResult.items.map(mapTaskDto),
3106
+ };
3107
+ }));
2043
3108
  }
2044
3109
  getCallTasks(callId) {
2045
3110
  if (this.useMock)
@@ -2083,7 +3148,7 @@ class AiReceptionistApiService {
2083
3148
  return this.mock.putSettings(next);
2084
3149
  return this.http
2085
3150
  .put(`${this.arBase}/settings`, { settings: mapSettingsToRequest(next) })
2086
- .pipe(switchMap(() => this.getSettings()));
3151
+ .pipe(switchMap(() => this.getSettings()), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to save agent settings')))));
2087
3152
  }
2088
3153
  /** ElevenLabs spoken sample for Agent Settings voice picker. */
2089
3154
  previewVoice(voiceId) {
@@ -2093,15 +3158,77 @@ class AiReceptionistApiService {
2093
3158
  responseType: 'blob',
2094
3159
  });
2095
3160
  }
2096
- getAudit(q) {
3161
+ /**
3162
+ * Knowledge Base test: draft an answer from whatToSay / whatNotToSay and
3163
+ * return spoken audio in the practice (or selected) voice.
3164
+ */
3165
+ testKnowledgeAnswer(params) {
3166
+ if (this.useMock) {
3167
+ return this.mock.testKnowledgeAnswer(params);
3168
+ }
3169
+ return this.http
3170
+ .post(`${this.arBase}/settings/test-knowledge`, params)
3171
+ .pipe(map((body) => {
3172
+ const answer = String(body?.answer ?? '').trim();
3173
+ const b64 = String(body?.audioBase64 ?? '').trim();
3174
+ if (!answer || !b64) {
3175
+ throw new Error('Test response missing answer or audio');
3176
+ }
3177
+ const contentType = String(body?.contentType ?? 'audio/mpeg');
3178
+ const binary = atob(b64);
3179
+ const bytes = new Uint8Array(binary.length);
3180
+ for (let i = 0; i < binary.length; i++)
3181
+ bytes[i] = binary.charCodeAt(i);
3182
+ return {
3183
+ answer,
3184
+ voiceId: String(body?.voiceId ?? params.voiceId ?? ''),
3185
+ audioBlob: new Blob([bytes], { type: contentType }),
3186
+ };
3187
+ }), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Could not generate a test answer.')))));
3188
+ }
3189
+ getAudit(q, opts) {
2097
3190
  if (this.useMock)
2098
- return this.mock.getAudit(q);
2099
- let params = new HttpParams();
3191
+ return this.mock.getAudit(q, undefined, opts);
3192
+ const page = opts?.page ?? 1;
3193
+ const limit = opts?.limit ?? DEFAULT_PAGE_SIZE;
3194
+ let params = new HttpParams().set('page', String(page)).set('limit', String(limit));
2100
3195
  if (q)
2101
3196
  params = params.set('search', q);
2102
- return this.http.get(`${this.arBase}/audit`, { params }).pipe(map((body) => ({
2103
- data: asAuditList(body).map((e, i) => mapAuditEventDto(e, i)),
2104
- })));
3197
+ return this.http.get(`${this.arBase}/audit`, { params }).pipe(map((body) => {
3198
+ const pageResult = asPaginatedResult(body, page, limit);
3199
+ return {
3200
+ ...pageResult,
3201
+ items: asAuditList(body).map((e, i) => mapAuditEventDto(e, i)),
3202
+ };
3203
+ }));
3204
+ }
3205
+ /**
3206
+ * Agent activity feed: patients created, appointments booked, tasks from AI.
3207
+ * GET /api/ai-receptionist/activities
3208
+ */
3209
+ getActivities(filters) {
3210
+ if (this.useMock)
3211
+ return this.mock.getActivities(filters);
3212
+ const page = filters?.page ?? 1;
3213
+ const limit = filters?.limit ?? DEFAULT_PAGE_SIZE;
3214
+ let params = new HttpParams().set('page', String(page)).set('limit', String(limit));
3215
+ if (filters?.search?.trim())
3216
+ params = params.set('search', filters.search.trim());
3217
+ if (filters?.types?.trim())
3218
+ params = params.set('types', filters.types.trim());
3219
+ if (filters?.from?.trim())
3220
+ params = params.set('from', filters.from.trim());
3221
+ if (filters?.to?.trim())
3222
+ params = params.set('to', filters.to.trim());
3223
+ if (filters?.callId?.trim())
3224
+ params = params.set('callId', filters.callId.trim());
3225
+ return this.http.get(`${this.arBase}/activities`, { params }).pipe(map((body) => {
3226
+ const pageResult = asPaginatedResult(body, page, limit);
3227
+ return {
3228
+ ...pageResult,
3229
+ items: pageResult.items.map((row, i) => mapAgentActivity(row, i)),
3230
+ };
3231
+ }));
2105
3232
  }
2106
3233
  getSetup() {
2107
3234
  if (this.useMock)
@@ -2219,6 +3346,62 @@ class AiReceptionistApiService {
2219
3346
  return throwError(() => ({ message: detail || 'Failed to place outbound call.' }));
2220
3347
  }));
2221
3348
  }
3349
+ /**
3350
+ * Patient typeahead for outbound dialer.
3351
+ * Tries `/api/v1/patients/search`, then `/api/v1/patients?q=`, then by-phone for digit queries.
3352
+ */
3353
+ searchPatientsForOutbound(q, limit = 10) {
3354
+ const query = q.trim();
3355
+ if (!query)
3356
+ return of([]);
3357
+ if (this.useMock)
3358
+ return this.mock.searchPatientsForOutbound(query, limit);
3359
+ const params = new HttpParams().set('q', query).set('limit', String(limit));
3360
+ const searchUrl = this.paths.v1Path('/patients/search');
3361
+ const listUrl = this.paths.v1Path('/patients');
3362
+ return this.http.get(searchUrl, { params }).pipe(map((body) => this.mapPatientHits(body)), catchError(() => this.http.get(listUrl, { params }).pipe(map((body) => this.mapPatientHits(body)), catchError(() => of([])))), switchMap((hits) => {
3363
+ if (hits.length > 0)
3364
+ return of(hits);
3365
+ const phone = toE164Phone(query);
3366
+ if (!phone && !/\d{7,}/.test(query))
3367
+ return of([]);
3368
+ return this.http
3369
+ .get(this.paths.v1Path('/patients/by-phone'), {
3370
+ params: new HttpParams().set('phone', phone || query.replace(/\D/g, '')),
3371
+ })
3372
+ .pipe(map((body) => this.mapPatientHits(body)), catchError(() => of([])));
3373
+ }));
3374
+ }
3375
+ /** Upcoming appointments for a patient (dialer picker). */
3376
+ listPatientAppointmentsForOutbound(patientId, limit = 20) {
3377
+ const id = patientId.trim();
3378
+ if (!id)
3379
+ return of([]);
3380
+ if (this.useMock)
3381
+ return this.mock.listPatientAppointmentsForOutbound(id, limit);
3382
+ const from = new Date();
3383
+ from.setHours(0, 0, 0, 0);
3384
+ const params = new HttpParams()
3385
+ .set('patientId', id)
3386
+ .set('patient_id', id)
3387
+ .set('from', from.toISOString())
3388
+ .set('limit', String(limit));
3389
+ const v1Url = this.paths.v1Path('/appointments');
3390
+ const apiUrl = this.paths.apiPath('/appointments');
3391
+ // Prefer /api/appointments (current host contract), then /api/v1/appointments.
3392
+ return this.http.get(apiUrl, { params }).pipe(map((body) => this.mapAppointmentHits(body)), catchError(() => this.http.get(v1Url, { params }).pipe(map((body) => this.mapAppointmentHits(body)), catchError(() => of([])))));
3393
+ }
3394
+ mapPatientHits(body) {
3395
+ return extractApiRows(body)
3396
+ .map(mapOutboundPatientHit)
3397
+ .filter((p) => !!p);
3398
+ }
3399
+ mapAppointmentHits(body) {
3400
+ return extractApiRows(body)
3401
+ .map(mapOutboundAppointmentHit)
3402
+ .filter((a) => !!a)
3403
+ .sort((a, b) => String(a.startTime ?? '').localeCompare(String(b.startTime ?? '')));
3404
+ }
2222
3405
  postCallAction(id, action) {
2223
3406
  const enc = encodeURIComponent(id);
2224
3407
  return this.http
@@ -2242,416 +3425,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
2242
3425
  args: [{ providedIn: 'root' }]
2243
3426
  }] });
2244
3427
 
2245
- /**
2246
- * Shared source of truth for Ava online/offline across topbar + module nav.
2247
- * Always re-fetches GET /status after mutations.
2248
- */
2249
- class AgentOnlineService {
2250
- constructor() {
2251
- this.api = inject(AiReceptionistApiService);
2252
- /** true = online, false = offline, null = unknown / not loaded */
2253
- this._online = signal(null);
2254
- this._loading = signal(false);
2255
- this._toggling = signal(false);
2256
- this._error = signal(null);
2257
- this._readiness = signal(null);
2258
- this.online = this._online.asReadonly();
2259
- this.loading = this._loading.asReadonly();
2260
- this.toggling = this._toggling.asReadonly();
2261
- this.error = this._error.asReadonly();
2262
- this.readiness = this._readiness.asReadonly();
2263
- }
2264
- refresh() {
2265
- this._loading.set(true);
2266
- this._error.set(null);
2267
- this.api.getStatus().subscribe({
2268
- next: ({ data }) => {
2269
- this._online.set(data.agentOn);
2270
- this._readiness.set(data.readiness);
2271
- this._loading.set(false);
2272
- },
2273
- error: (err) => {
2274
- this._online.set(null);
2275
- this._error.set(err.message ?? 'Could not load agent status.');
2276
- this._loading.set(false);
2277
- },
2278
- });
2279
- }
2280
- /**
2281
- * PUT /agent-online then re-fetch GET /status.
2282
- */
2283
- setOnline(enabled) {
2284
- this._toggling.set(true);
2285
- this._error.set(null);
2286
- return this.api.setAgentOnline(enabled).pipe(switchMap(() => this.api.getStatus().pipe(catchError((err) => {
2287
- this._error.set(err.message ?? 'Status refresh failed after toggle.');
2288
- return of(null);
2289
- }))), tap((result) => {
2290
- if (result) {
2291
- this._online.set(result.data.agentOn);
2292
- this._readiness.set(result.data.readiness);
2293
- }
2294
- else {
2295
- // Toggle likely succeeded; use requested value until refresh works.
2296
- this._online.set(enabled);
2297
- }
2298
- }), catchError((err) => {
2299
- this._error.set(err.message ?? 'Failed to update agent online status.');
2300
- return throwError(() => err);
2301
- }), finalize(() => this._toggling.set(false)));
2302
- }
2303
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2304
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, providedIn: 'root' }); }
2305
- }
2306
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, decorators: [{
2307
- type: Injectable,
2308
- args: [{ providedIn: 'root' }]
2309
- }] });
2310
-
2311
- class EdsIconComponent {
2312
- constructor() {
2313
- this.name = input.required();
2314
- this.size = input(20);
2315
- this.fill = input(0);
2316
- this.weight = input(400);
2317
- this.variation = computed(() => `'FILL' ${this.fill()}, 'wght' ${this.weight()}, 'GRAD' 0, 'opsz' ${this.size()}`);
2318
- }
2319
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2320
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.17", type: EdsIconComponent, isStandalone: true, selector: "eds-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, weight: { classPropertyName: "weight", publicName: "weight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `<span class="eds-icon" [style.font-size.px]="size()" [style.font-variation-settings]="variation()">{{ name() }}</span>`, isInline: true, styles: [":host{display:inline-flex;line-height:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2321
- }
2322
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, decorators: [{
2323
- type: Component,
2324
- args: [{ selector: 'eds-icon', standalone: true, template: `<span class="eds-icon" [style.font-size.px]="size()" [style.font-variation-settings]="variation()">{{ name() }}</span>`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:inline-flex;line-height:0}\n"] }]
2325
- }] });
2326
-
2327
- class EdsPulseDotComponent {
2328
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2329
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: EdsPulseDotComponent, isStandalone: true, selector: "eds-pulse-dot", ngImport: i0, template: `<span class="eds-pulse-dot"><span class="eds-pulse-dot__core"></span></span>`, isInline: true, styles: [".eds-pulse-dot{position:relative;display:inline-flex;width:8px;height:8px}.eds-pulse-dot__core{width:8px;height:8px;border-radius:50%;background:var(--green-500)}.eds-pulse-dot__core:after{content:\"\";position:absolute;inset:0;border-radius:50%;background:var(--green-500);animation:eds-ping 1.4s cubic-bezier(0,0,.2,1) infinite}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2330
- }
2331
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, decorators: [{
2332
- type: Component,
2333
- args: [{ selector: 'eds-pulse-dot', standalone: true, template: `<span class="eds-pulse-dot"><span class="eds-pulse-dot__core"></span></span>`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".eds-pulse-dot{position:relative;display:inline-flex;width:8px;height:8px}.eds-pulse-dot__core{width:8px;height:8px;border-radius:50%;background:var(--green-500)}.eds-pulse-dot__core:after{content:\"\";position:absolute;inset:0;border-radius:50%;background:var(--green-500);animation:eds-ping 1.4s cubic-bezier(0,0,.2,1) infinite}\n"] }]
2334
- }] });
2335
-
2336
- class EdsModuleNavComponent {
2337
- constructor() {
2338
- this.config = inject(VOICE_RECEPTIONIST_CONFIG);
2339
- this.session = inject(VOICE_RECEPTIONIST_SESSION);
2340
- this.agentOnline = inject(AgentOnlineService);
2341
- this.agentOn = this.agentOnline.online;
2342
- this.includePracticeNav = !!this.config.includePracticeNav;
2343
- this.receptionistNav = [
2344
- { id: 'dashboard', label: 'Dashboard', icon: 'space_dashboard', route: 'dashboard' },
2345
- { id: 'live', label: 'Live Calls', icon: 'graphic_eq', route: 'live', pulse: true },
2346
- { id: 'history', label: 'Call History', icon: 'history', route: 'history' },
2347
- { id: 'analytics', label: 'Analytics', icon: 'monitoring', route: 'analytics' },
2348
- { id: 'knowledge', label: 'Knowledge Base', icon: 'menu_book', route: 'knowledge' },
2349
- { id: 'settings', label: 'Agent Settings', icon: 'tune', route: 'settings' },
2350
- { id: 'audit', label: 'Audit Log', icon: 'verified', route: 'audit' },
2351
- ];
2352
- this.practiceNav = [
2353
- { id: 'patients', label: 'Patients', icon: 'groups', route: 'patients' },
2354
- { id: 'providers', label: 'Providers', icon: 'medical_services', route: 'providers' },
2355
- { id: 'appointments', label: 'Appointments', icon: 'calendar_month', route: 'appointments' },
2356
- { id: 'admin', label: 'Admin', icon: 'admin_panel_settings', route: 'admin', adminOnly: true },
2357
- ];
2358
- if (this.agentOnline.online() === null && !this.agentOnline.loading()) {
2359
- this.agentOnline.refresh();
2360
- }
2361
- }
2362
- showNavItem(item) {
2363
- return !item.adminOnly || !!this.session?.isAdmin();
2364
- }
2365
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2366
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsModuleNavComponent, isStandalone: true, selector: "eds-module-nav", ngImport: i0, template: "<nav class=\"eds-modnav\" aria-label=\"AI Receptionist\">\n <div class=\"eds-modnav-header\">\n <span class=\"eds-modnav-icon eds-gradient-brand\">\n <eds-icon name=\"graphic_eq\" [size]=\"18\" [fill]=\"1\" />\n </span>\n <div class=\"eds-modnav-titles\">\n <span class=\"eds-modnav-name\">AI Receptionist</span>\n <span class=\"eds-modnav-status\">\n @if (agentOn() === true) {\n <eds-pulse-dot />\n Answering calls\n } @else if (agentOn() === false) {\n Offline \u2014 front desk\n } @else {\n Status unknown\n }\n </span>\n </div>\n </div>\n\n <div class=\"eds-modnav-links\">\n @for (item of receptionistNav; track item.id) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [routerLinkActiveOptions]=\"{ exact: item.route === 'dashboard' }\"\n >\n <eds-icon [name]=\"item.icon\" [size]=\"18\" />\n <span class=\"eds-modnav-label\">{{ item.label }}</span>\n @if (item.pulse) {\n <eds-pulse-dot class=\"eds-modnav-pulse\" />\n }\n @if (item.badge) {\n <span class=\"eds-modnav-badge\">{{ item.badge }}</span>\n }\n </a>\n }\n </div>\n\n @if (includePracticeNav) {\n <p class=\"eds-modnav-section\">Practice</p>\n <div class=\"eds-modnav-links eds-modnav-links--practice\">\n @for (item of practiceNav; track item.id) {\n @if (showNavItem(item)) {\n <a [routerLink]=\"item.route\" routerLinkActive=\"active\">\n <eds-icon [name]=\"item.icon\" [size]=\"18\" />\n <span class=\"eds-modnav-label\">{{ item.label }}</span>\n </a>\n }\n }\n </div>\n }\n</nav>\n", styles: [":host{display:block;flex-shrink:0}.eds-modnav{width:var(--eds-nav-width);min-height:100vh;background:var(--surface-l0);border-right:1px solid var(--border-default);display:flex;flex-direction:column;padding:16px 12px;position:sticky;top:0;height:100vh;overflow-y:auto}.eds-modnav-header{display:flex;align-items:center;gap:10px;padding:4px 8px 16px;border-bottom:1px solid var(--border-default);margin-bottom:12px}.eds-modnav-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--eds-radius-md);color:#fff}.eds-modnav-titles{display:flex;flex-direction:column;gap:2px;min-width:0}.eds-modnav-name{font-family:var(--font-inter);font-size:14px;font-weight:600;color:var(--text-primary)}.eds-modnav-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--green-500);font-weight:500}.eds-modnav-links{display:flex;flex-direction:column;gap:2px}.eds-modnav-links a{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--eds-radius-md);text-decoration:none;color:var(--text-secondary);font-size:13px;font-weight:500;position:relative}.eds-modnav-links a:hover{background:var(--surface-l1);color:var(--text-primary)}.eds-modnav-links a.active{background:#3681f41a;color:var(--brand-600)}.eds-modnav-links--practice{margin-bottom:12px}.eds-modnav-section{margin:14px 10px 6px;font-family:var(--font-inter);font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--gray-500)}.eds-modnav-label{flex:1;min-width:0}.eds-modnav-pulse{margin-left:auto}.eds-modnav-badge{margin-left:auto;min-width:18px;height:18px;padding:0 5px;border-radius:999px;background:var(--brand-600);color:#fff;font-size:10px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}.eds-readiness-card{margin-top:auto;padding:12px;border-radius:var(--eds-radius-lg);border:1px solid var(--border-default);background:var(--surface-l1);text-decoration:none;color:inherit;transition:box-shadow .15s ease}.eds-readiness-card:hover{box-shadow:var(--eds-shadow-md)}.eds-readiness-top{display:flex;justify-content:space-between;margin-bottom:8px}.eds-readiness-bar{height:6px;background:var(--gray-200);border-radius:999px;overflow:hidden}.eds-readiness-bar span{display:block;height:100%;background:linear-gradient(90deg,var(--brand-500),var(--violet-500));border-radius:999px}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { kind: "component", type: EdsPulseDotComponent, selector: "eds-pulse-dot" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2367
- }
2368
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, decorators: [{
2369
- type: Component,
2370
- args: [{ selector: 'eds-module-nav', imports: [RouterLink, RouterLinkActive, EdsIconComponent, EdsPulseDotComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"eds-modnav\" aria-label=\"AI Receptionist\">\n <div class=\"eds-modnav-header\">\n <span class=\"eds-modnav-icon eds-gradient-brand\">\n <eds-icon name=\"graphic_eq\" [size]=\"18\" [fill]=\"1\" />\n </span>\n <div class=\"eds-modnav-titles\">\n <span class=\"eds-modnav-name\">AI Receptionist</span>\n <span class=\"eds-modnav-status\">\n @if (agentOn() === true) {\n <eds-pulse-dot />\n Answering calls\n } @else if (agentOn() === false) {\n Offline \u2014 front desk\n } @else {\n Status unknown\n }\n </span>\n </div>\n </div>\n\n <div class=\"eds-modnav-links\">\n @for (item of receptionistNav; track item.id) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [routerLinkActiveOptions]=\"{ exact: item.route === 'dashboard' }\"\n >\n <eds-icon [name]=\"item.icon\" [size]=\"18\" />\n <span class=\"eds-modnav-label\">{{ item.label }}</span>\n @if (item.pulse) {\n <eds-pulse-dot class=\"eds-modnav-pulse\" />\n }\n @if (item.badge) {\n <span class=\"eds-modnav-badge\">{{ item.badge }}</span>\n }\n </a>\n }\n </div>\n\n @if (includePracticeNav) {\n <p class=\"eds-modnav-section\">Practice</p>\n <div class=\"eds-modnav-links eds-modnav-links--practice\">\n @for (item of practiceNav; track item.id) {\n @if (showNavItem(item)) {\n <a [routerLink]=\"item.route\" routerLinkActive=\"active\">\n <eds-icon [name]=\"item.icon\" [size]=\"18\" />\n <span class=\"eds-modnav-label\">{{ item.label }}</span>\n </a>\n }\n }\n </div>\n }\n</nav>\n", styles: [":host{display:block;flex-shrink:0}.eds-modnav{width:var(--eds-nav-width);min-height:100vh;background:var(--surface-l0);border-right:1px solid var(--border-default);display:flex;flex-direction:column;padding:16px 12px;position:sticky;top:0;height:100vh;overflow-y:auto}.eds-modnav-header{display:flex;align-items:center;gap:10px;padding:4px 8px 16px;border-bottom:1px solid var(--border-default);margin-bottom:12px}.eds-modnav-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--eds-radius-md);color:#fff}.eds-modnav-titles{display:flex;flex-direction:column;gap:2px;min-width:0}.eds-modnav-name{font-family:var(--font-inter);font-size:14px;font-weight:600;color:var(--text-primary)}.eds-modnav-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--green-500);font-weight:500}.eds-modnav-links{display:flex;flex-direction:column;gap:2px}.eds-modnav-links a{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--eds-radius-md);text-decoration:none;color:var(--text-secondary);font-size:13px;font-weight:500;position:relative}.eds-modnav-links a:hover{background:var(--surface-l1);color:var(--text-primary)}.eds-modnav-links a.active{background:#3681f41a;color:var(--brand-600)}.eds-modnav-links--practice{margin-bottom:12px}.eds-modnav-section{margin:14px 10px 6px;font-family:var(--font-inter);font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--gray-500)}.eds-modnav-label{flex:1;min-width:0}.eds-modnav-pulse{margin-left:auto}.eds-modnav-badge{margin-left:auto;min-width:18px;height:18px;padding:0 5px;border-radius:999px;background:var(--brand-600);color:#fff;font-size:10px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}.eds-readiness-card{margin-top:auto;padding:12px;border-radius:var(--eds-radius-lg);border:1px solid var(--border-default);background:var(--surface-l1);text-decoration:none;color:inherit;transition:box-shadow .15s ease}.eds-readiness-card:hover{box-shadow:var(--eds-shadow-md)}.eds-readiness-top{display:flex;justify-content:space-between;margin-bottom:8px}.eds-readiness-bar{height:6px;background:var(--gray-200);border-radius:999px;overflow:hidden}.eds-readiness-bar span{display:block;height:100%;background:linear-gradient(90deg,var(--brand-500),var(--violet-500));border-radius:999px}\n"] }]
2371
- }], ctorParameters: () => [] });
2372
-
2373
- /**
2374
- * Lightweight, reusable modal shell.
2375
- *
2376
- * Renders a centered card on top of a dimmed backdrop. Closes on backdrop
2377
- * click, ESC key, or via the close button. Slot the modal body using
2378
- * <ng-content>.
2379
- */
2380
- class ModalComponent {
2381
- constructor() {
2382
- this.maxWidth = '640px';
2383
- this.closed = new EventEmitter();
2384
- }
2385
- onEscape() {
2386
- this.closed.emit();
2387
- }
2388
- onBackdropClick(event) {
2389
- if (event.target === event.currentTarget) {
2390
- this.closed.emit();
2391
- }
2392
- }
2393
- close() {
2394
- this.closed.emit();
2395
- }
2396
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2397
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: ModalComponent, isStandalone: true, selector: "app-modal", inputs: { title: "title", subtitle: "subtitle", maxWidth: "maxWidth" }, outputs: { closed: "closed" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\" role=\"presentation\">\n <div\n class=\"modal-card\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title\"\n [style.max-width]=\"maxWidth\"\n >\n <header class=\"modal-header\">\n <div class=\"modal-titles\">\n <h2 class=\"modal-title\">{{ title }}</h2>\n @if (subtitle) {\n <p class=\"modal-subtitle\">{{ subtitle }}</p>\n }\n </div>\n <button type=\"button\" class=\"modal-close\" (click)=\"close()\" aria-label=\"Close dialog\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </header>\n\n <div class=\"modal-body\">\n <ng-content />\n </div>\n </div>\n</div>\n", styles: [":host{display:contents}.modal-backdrop{position:fixed;inset:0;background:#141e326b;backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:1000;animation:fade-in .14s ease}.modal-card{width:100%;background:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border);box-shadow:var(--shadow-modal);max-height:calc(100vh - 48px);display:flex;flex-direction:column;overflow:hidden;animation:pop-in .14s ease}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:20px 24px 14px;border-bottom:1px solid var(--color-border)}.modal-titles{display:flex;flex-direction:column;gap:4px;min-width:0}.modal-title{margin:0;font-size:16px;font-weight:600;letter-spacing:-.005em;color:var(--color-text)}.modal-subtitle{margin:0;font-size:13px;color:var(--color-text-muted)}.modal-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--radius-md);border:1px solid transparent;background:transparent;color:var(--color-text-muted);transition:background .12s ease,color .12s ease}.modal-close:hover{background:var(--color-surface-alt);color:var(--color-text)}.modal-close:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.modal-body{padding:20px 24px 24px;overflow-y:auto}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes pop-in{0%{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2398
- }
2399
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, decorators: [{
2400
- type: Component,
2401
- args: [{ selector: 'app-modal', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\" role=\"presentation\">\n <div\n class=\"modal-card\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title\"\n [style.max-width]=\"maxWidth\"\n >\n <header class=\"modal-header\">\n <div class=\"modal-titles\">\n <h2 class=\"modal-title\">{{ title }}</h2>\n @if (subtitle) {\n <p class=\"modal-subtitle\">{{ subtitle }}</p>\n }\n </div>\n <button type=\"button\" class=\"modal-close\" (click)=\"close()\" aria-label=\"Close dialog\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </header>\n\n <div class=\"modal-body\">\n <ng-content />\n </div>\n </div>\n</div>\n", styles: [":host{display:contents}.modal-backdrop{position:fixed;inset:0;background:#141e326b;backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:1000;animation:fade-in .14s ease}.modal-card{width:100%;background:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border);box-shadow:var(--shadow-modal);max-height:calc(100vh - 48px);display:flex;flex-direction:column;overflow:hidden;animation:pop-in .14s ease}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:20px 24px 14px;border-bottom:1px solid var(--color-border)}.modal-titles{display:flex;flex-direction:column;gap:4px;min-width:0}.modal-title{margin:0;font-size:16px;font-weight:600;letter-spacing:-.005em;color:var(--color-text)}.modal-subtitle{margin:0;font-size:13px;color:var(--color-text-muted)}.modal-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--radius-md);border:1px solid transparent;background:transparent;color:var(--color-text-muted);transition:background .12s ease,color .12s ease}.modal-close:hover{background:var(--color-surface-alt);color:var(--color-text)}.modal-close:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.modal-body{padding:20px 24px 24px;overflow-y:auto}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes pop-in{0%{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] }]
2402
- }], propDecorators: { title: [{
2403
- type: Input,
2404
- args: [{ required: true }]
2405
- }], subtitle: [{
2406
- type: Input
2407
- }], maxWidth: [{
2408
- type: Input
2409
- }], closed: [{
2410
- type: Output
2411
- }], onEscape: [{
2412
- type: HostListener,
2413
- args: ['document:keydown.escape']
2414
- }] } });
2415
-
2416
- class EdsButtonComponent {
2417
- constructor() {
2418
- this.variant = input('secondary');
2419
- this.type = input('button');
2420
- this.disabled = input(false);
2421
- }
2422
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2423
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.17", type: EdsButtonComponent, isStandalone: true, selector: "eds-button", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2424
- <button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
2425
- <ng-content />
2426
- </button>
2427
- `, isInline: true, styles: [".eds-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 14px;border-radius:var(--eds-radius-md);border:1px solid transparent;font-family:var(--font-inter);font-size:13px;font-weight:500;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}.eds-btn:disabled{opacity:.5;cursor:not-allowed}.eds-btn[data-variant=primary]{background:var(--brand-600);color:#fff}.eds-btn[data-variant=primary]:hover:not(:disabled){background:var(--brand-500)}.eds-btn[data-variant=secondary]{background:var(--surface-l0);color:var(--text-primary);border-color:var(--border-default)}.eds-btn[data-variant=secondary]:hover:not(:disabled){background:var(--surface-l1)}.eds-btn[data-variant=ghost]{background:transparent;color:var(--text-secondary)}.eds-btn[data-variant=ghost]:hover:not(:disabled){background:var(--gray-100);color:var(--text-primary)}.eds-btn[data-variant=danger]{background:#f037401a;color:var(--red-500);border-color:#f0374040}.eds-btn[data-variant=positive]{background:#0abf521f;color:var(--green-500);border-color:#0abf5240}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2428
- }
2429
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, decorators: [{
2430
- type: Component,
2431
- args: [{ selector: 'eds-button', standalone: true, template: `
2432
- <button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
2433
- <ng-content />
2434
- </button>
2435
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".eds-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 14px;border-radius:var(--eds-radius-md);border:1px solid transparent;font-family:var(--font-inter);font-size:13px;font-weight:500;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}.eds-btn:disabled{opacity:.5;cursor:not-allowed}.eds-btn[data-variant=primary]{background:var(--brand-600);color:#fff}.eds-btn[data-variant=primary]:hover:not(:disabled){background:var(--brand-500)}.eds-btn[data-variant=secondary]{background:var(--surface-l0);color:var(--text-primary);border-color:var(--border-default)}.eds-btn[data-variant=secondary]:hover:not(:disabled){background:var(--surface-l1)}.eds-btn[data-variant=ghost]{background:transparent;color:var(--text-secondary)}.eds-btn[data-variant=ghost]:hover:not(:disabled){background:var(--gray-100);color:var(--text-primary)}.eds-btn[data-variant=danger]{background:#f037401a;color:var(--red-500);border-color:#f0374040}.eds-btn[data-variant=positive]{background:#0abf521f;color:var(--green-500);border-color:#0abf5240}\n"] }]
2436
- }] });
2437
-
2438
- const PAGE_TITLES = {
2439
- dashboard: 'Dashboard',
2440
- live: 'Live Calls',
2441
- history: 'Call History',
2442
- tasks: 'Tasks & Follow-ups',
2443
- analytics: 'Analytics',
2444
- settings: 'Agent Settings',
2445
- knowledge: 'Knowledge Base',
2446
- audit: 'Audit Log',
2447
- wizard: 'Setup Wizard',
2448
- patients: 'Patients',
2449
- providers: 'Providers',
2450
- appointments: 'Appointments',
2451
- admin: 'Admin',
2452
- };
2453
- class EdsTopbarComponent {
2454
- constructor() {
2455
- this.router = inject(Router);
2456
- this.sessionAdapter = inject(VOICE_RECEPTIONIST_SESSION);
2457
- this.toast = inject(ToastService);
2458
- this.agentOnline = inject(AgentOnlineService);
2459
- this.pageTitle = signal('Dashboard');
2460
- this.confirmOffOpen = signal(false);
2461
- this.online = this.agentOnline.online;
2462
- this.loading = this.agentOnline.loading;
2463
- this.toggling = this.agentOnline.toggling;
2464
- this.statusError = this.agentOnline.error;
2465
- this.userEmail = computed(() => this.sessionAdapter?.email() ?? null);
2466
- this.showUser = computed(() => !!this.userEmail());
2467
- this.showLogout = computed(() => !!this.sessionAdapter);
2468
- this.userInitials = computed(() => {
2469
- const email = this.userEmail()?.trim() ?? '';
2470
- if (!email)
2471
- return '?';
2472
- const local = email.split('@')[0] ?? '';
2473
- const parts = local.split(/[._-]+/).filter(Boolean);
2474
- if (parts.length >= 2) {
2475
- return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
2476
- }
2477
- return local.slice(0, 2).toUpperCase() || '?';
2478
- });
2479
- this.statusLabel = computed(() => {
2480
- if (this.loading() && this.online() === null)
2481
- return 'Checking status…';
2482
- if (this.online() === null)
2483
- return 'Status unknown';
2484
- return this.online()
2485
- ? 'AI receptionist is answering calls'
2486
- : 'AI is offline — calls go to front desk';
2487
- });
2488
- this.switchDisabled = computed(() => this.loading() || this.toggling() || this.online() === null);
2489
- this.agentOnline.refresh();
2490
- this.updateTitle(this.router.url);
2491
- this.router.events
2492
- .pipe(filter((e) => e instanceof NavigationEnd))
2493
- .subscribe((e) => this.updateTitle(e.urlAfterRedirects));
2494
- }
2495
- onToggleClick() {
2496
- if (this.switchDisabled())
2497
- return;
2498
- if (this.online()) {
2499
- this.confirmOffOpen.set(true);
2500
- return;
2501
- }
2502
- this.applyOnline(true);
2503
- }
2504
- cancelConfirmOff() {
2505
- this.confirmOffOpen.set(false);
2506
- }
2507
- confirmTurnOff() {
2508
- this.confirmOffOpen.set(false);
2509
- this.applyOnline(false);
2510
- }
2511
- applyOnline(enabled) {
2512
- this.agentOnline.setOnline(enabled).subscribe({
2513
- next: () => {
2514
- this.toast.show(enabled
2515
- ? 'AI receptionist is online — Ava will answer inbound calls.'
2516
- : 'AI receptionist is offline — calls go to the front desk.', 'success');
2517
- },
2518
- error: (err) => {
2519
- this.toast.show(err.message ?? 'Failed to update agent status.', 'error');
2520
- this.agentOnline.refresh();
2521
- },
2522
- });
2523
- }
2524
- logout() {
2525
- this.sessionAdapter?.logout();
2526
- }
2527
- updateTitle(url) {
2528
- const base = url.split('?')[0].split(';')[0];
2529
- const segments = base.split('/').filter(Boolean);
2530
- const last = segments[segments.length - 1] ?? '';
2531
- const prev = segments[segments.length - 2] ?? '';
2532
- if (prev === 'live' && last && last !== 'live') {
2533
- this.pageTitle.set('Live Monitor');
2534
- return;
2535
- }
2536
- if (prev === 'history' && last && last !== 'history') {
2537
- this.pageTitle.set('Call Detail');
2538
- return;
2539
- }
2540
- if (prev === 'settings' && last === 'outbound-purposes') {
2541
- this.pageTitle.set('Outbound Purposes');
2542
- return;
2543
- }
2544
- this.pageTitle.set(PAGE_TITLES[last] ?? 'AI Receptionist');
2545
- }
2546
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2547
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsTopbarComponent, isStandalone: true, selector: "eds-topbar", ngImport: i0, template: "<header class=\"eds-topbar\">\n <div class=\"eds-topbar-left\">\n <nav class=\"eds-breadcrumb eds-body-sm\" aria-label=\"Breadcrumb\">\n <span>AI Receptionist</span>\n <eds-icon name=\"chevron_right\" [size]=\"16\" />\n <span class=\"eds-breadcrumb-current\">{{ pageTitle() }}</span>\n </nav>\n </div>\n\n <div class=\"eds-topbar-right\">\n <div class=\"eds-agent-online\" [attr.data-state]=\"online() === null ? 'unknown' : online() ? 'on' : 'off'\">\n <button\n type=\"button\"\n class=\"eds-agent-toggle\"\n [disabled]=\"switchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n [attr.aria-busy]=\"toggling()\"\n [title]=\"statusLabel()\"\n (click)=\"onToggleClick()\"\n >\n <span\n class=\"eds-agent-toggle__track\"\n [class.on]=\"online() === true\"\n [class.unknown]=\"online() === null\"\n >\n <span class=\"eds-agent-toggle__thumb\"></span>\n </span>\n <span class=\"eds-agent-toggle__copy\">\n <span class=\"eds-body-sm eds-agent-toggle__label\">\n @if (toggling()) {\n Updating\u2026\n } @else if (online() === true) {\n Agent online\n } @else if (online() === false) {\n Agent offline\n } @else {\n Status unknown\n }\n </span>\n <span class=\"eds-agent-toggle__hint\">{{ statusLabel() }}</span>\n </span>\n </button>\n @if (statusError() && online() === null) {\n <button type=\"button\" class=\"eds-agent-retry eds-body-sm\" (click)=\"agentOnline.refresh()\">\n Retry\n </button>\n }\n </div>\n\n @if (showUser()) {\n <div class=\"eds-topbar-user\" [title]=\"userEmail()!\">\n <span class=\"eds-topbar-user__avatar\">{{ userInitials() }}</span>\n <span class=\"eds-topbar-user__email eds-body-sm\">{{ userEmail() }}</span>\n </div>\n }\n\n @if (showLogout()) {\n <button type=\"button\" class=\"eds-topbar-logout\" (click)=\"logout()\" aria-label=\"Sign out\" title=\"Sign out\">\n <eds-icon name=\"logout\" [size]=\"18\" />\n <span class=\"eds-body-sm\">Log out</span>\n </button>\n }\n </div>\n</header>\n\n@if (confirmOffOpen()) {\n <app-modal\n title=\"Turn off AI receptionist?\"\n subtitle=\"Inbound callers will be transferred to the front desk instead of Ava.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"eds-agent-confirm__body\">\n While offline, Ava will not answer calls. Callers are routed to the human receptionist\n at the front desk.\n </p>\n <div class=\"eds-agent-confirm__actions\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"cancelConfirmOff()\">Cancel</eds-button>\n <eds-button variant=\"danger\" type=\"button\" [disabled]=\"toggling()\" (click)=\"confirmTurnOff()\">\n Turn off\n </eds-button>\n </div>\n </app-modal>\n}\n", styles: [".eds-topbar{height:var(--eds-topbar-height);display:flex;align-items:center;gap:16px;padding:0 24px;background:var(--surface-l0);border-bottom:1px solid var(--border-default);position:sticky;top:0;z-index:10}.eds-topbar-left{flex-shrink:0}.eds-breadcrumb{display:inline-flex;align-items:center;gap:4px;color:var(--text-secondary)}.eds-breadcrumb-current{color:var(--text-primary);font-weight:600}.eds-topbar-right{display:flex;align-items:center;gap:12px;margin-left:auto}.eds-agent-online{display:inline-flex;align-items:center;gap:8px;max-width:min(360px,42vw)}.eds-agent-toggle{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--border-default);background:var(--surface-l0);border-radius:var(--eds-radius-md);cursor:pointer;padding:6px 10px;text-align:left;font:inherit;color:inherit}.eds-agent-toggle:disabled{opacity:.65;cursor:not-allowed}.eds-agent-toggle:hover:not(:disabled){background:var(--surface-l1)}.eds-agent-toggle__track{width:36px;height:20px;border-radius:999px;background:var(--gray-300);position:relative;transition:background .15s ease;flex-shrink:0}.eds-agent-toggle__track.on{background:var(--green-500)}.eds-agent-toggle__track.unknown{background:var(--gray-400)}.eds-agent-toggle__thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .15s ease;box-shadow:var(--eds-shadow-sm)}.on .eds-agent-toggle__thumb{transform:translate(16px)}.eds-agent-toggle__copy{display:flex;flex-direction:column;gap:1px;min-width:0}.eds-agent-toggle__label{font-weight:600;color:var(--text-primary);white-space:nowrap}.eds-agent-toggle__hint{font-size:11px;line-height:1.3;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.eds-agent-retry{border:none;background:transparent;color:var(--color-primary, #3681f4);cursor:pointer;padding:4px 6px;text-decoration:underline}.eds-agent-confirm__body{margin:0 0 20px;font-size:14px;line-height:1.5;color:var(--text-secondary)}.eds-agent-confirm__body code{font-size:12px}.eds-agent-confirm__actions{display:flex;justify-content:flex-end;gap:10px}@media(max-width:1100px){.eds-agent-toggle__hint{display:none}}.eds-topbar-user{display:inline-flex;align-items:center;gap:8px;max-width:200px;padding:4px 8px;border-radius:var(--eds-radius-md)}.eds-topbar-user__avatar{width:28px;height:28px;border-radius:50%;background:var(--gray-200);color:var(--gray-700);font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}.eds-topbar-user__email{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)}.eds-topbar-logout{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border-default);background:var(--surface-l0);color:var(--text-secondary);border-radius:var(--eds-radius-md);padding:6px 10px;cursor:pointer;font:inherit}.eds-topbar-logout:hover{background:var(--surface-l1);color:var(--text-primary);border-color:var(--gray-300)}@media(max-width:1100px){.eds-topbar-user__email{display:none}}\n"], dependencies: [{ kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2548
- }
2549
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, decorators: [{
2550
- type: Component,
2551
- args: [{ selector: 'eds-topbar', imports: [EdsIconComponent, EdsButtonComponent, ModalComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"eds-topbar\">\n <div class=\"eds-topbar-left\">\n <nav class=\"eds-breadcrumb eds-body-sm\" aria-label=\"Breadcrumb\">\n <span>AI Receptionist</span>\n <eds-icon name=\"chevron_right\" [size]=\"16\" />\n <span class=\"eds-breadcrumb-current\">{{ pageTitle() }}</span>\n </nav>\n </div>\n\n <div class=\"eds-topbar-right\">\n <div class=\"eds-agent-online\" [attr.data-state]=\"online() === null ? 'unknown' : online() ? 'on' : 'off'\">\n <button\n type=\"button\"\n class=\"eds-agent-toggle\"\n [disabled]=\"switchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n [attr.aria-busy]=\"toggling()\"\n [title]=\"statusLabel()\"\n (click)=\"onToggleClick()\"\n >\n <span\n class=\"eds-agent-toggle__track\"\n [class.on]=\"online() === true\"\n [class.unknown]=\"online() === null\"\n >\n <span class=\"eds-agent-toggle__thumb\"></span>\n </span>\n <span class=\"eds-agent-toggle__copy\">\n <span class=\"eds-body-sm eds-agent-toggle__label\">\n @if (toggling()) {\n Updating\u2026\n } @else if (online() === true) {\n Agent online\n } @else if (online() === false) {\n Agent offline\n } @else {\n Status unknown\n }\n </span>\n <span class=\"eds-agent-toggle__hint\">{{ statusLabel() }}</span>\n </span>\n </button>\n @if (statusError() && online() === null) {\n <button type=\"button\" class=\"eds-agent-retry eds-body-sm\" (click)=\"agentOnline.refresh()\">\n Retry\n </button>\n }\n </div>\n\n @if (showUser()) {\n <div class=\"eds-topbar-user\" [title]=\"userEmail()!\">\n <span class=\"eds-topbar-user__avatar\">{{ userInitials() }}</span>\n <span class=\"eds-topbar-user__email eds-body-sm\">{{ userEmail() }}</span>\n </div>\n }\n\n @if (showLogout()) {\n <button type=\"button\" class=\"eds-topbar-logout\" (click)=\"logout()\" aria-label=\"Sign out\" title=\"Sign out\">\n <eds-icon name=\"logout\" [size]=\"18\" />\n <span class=\"eds-body-sm\">Log out</span>\n </button>\n }\n </div>\n</header>\n\n@if (confirmOffOpen()) {\n <app-modal\n title=\"Turn off AI receptionist?\"\n subtitle=\"Inbound callers will be transferred to the front desk instead of Ava.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"eds-agent-confirm__body\">\n While offline, Ava will not answer calls. Callers are routed to the human receptionist\n at the front desk.\n </p>\n <div class=\"eds-agent-confirm__actions\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"cancelConfirmOff()\">Cancel</eds-button>\n <eds-button variant=\"danger\" type=\"button\" [disabled]=\"toggling()\" (click)=\"confirmTurnOff()\">\n Turn off\n </eds-button>\n </div>\n </app-modal>\n}\n", styles: [".eds-topbar{height:var(--eds-topbar-height);display:flex;align-items:center;gap:16px;padding:0 24px;background:var(--surface-l0);border-bottom:1px solid var(--border-default);position:sticky;top:0;z-index:10}.eds-topbar-left{flex-shrink:0}.eds-breadcrumb{display:inline-flex;align-items:center;gap:4px;color:var(--text-secondary)}.eds-breadcrumb-current{color:var(--text-primary);font-weight:600}.eds-topbar-right{display:flex;align-items:center;gap:12px;margin-left:auto}.eds-agent-online{display:inline-flex;align-items:center;gap:8px;max-width:min(360px,42vw)}.eds-agent-toggle{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--border-default);background:var(--surface-l0);border-radius:var(--eds-radius-md);cursor:pointer;padding:6px 10px;text-align:left;font:inherit;color:inherit}.eds-agent-toggle:disabled{opacity:.65;cursor:not-allowed}.eds-agent-toggle:hover:not(:disabled){background:var(--surface-l1)}.eds-agent-toggle__track{width:36px;height:20px;border-radius:999px;background:var(--gray-300);position:relative;transition:background .15s ease;flex-shrink:0}.eds-agent-toggle__track.on{background:var(--green-500)}.eds-agent-toggle__track.unknown{background:var(--gray-400)}.eds-agent-toggle__thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .15s ease;box-shadow:var(--eds-shadow-sm)}.on .eds-agent-toggle__thumb{transform:translate(16px)}.eds-agent-toggle__copy{display:flex;flex-direction:column;gap:1px;min-width:0}.eds-agent-toggle__label{font-weight:600;color:var(--text-primary);white-space:nowrap}.eds-agent-toggle__hint{font-size:11px;line-height:1.3;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.eds-agent-retry{border:none;background:transparent;color:var(--color-primary, #3681f4);cursor:pointer;padding:4px 6px;text-decoration:underline}.eds-agent-confirm__body{margin:0 0 20px;font-size:14px;line-height:1.5;color:var(--text-secondary)}.eds-agent-confirm__body code{font-size:12px}.eds-agent-confirm__actions{display:flex;justify-content:flex-end;gap:10px}@media(max-width:1100px){.eds-agent-toggle__hint{display:none}}.eds-topbar-user{display:inline-flex;align-items:center;gap:8px;max-width:200px;padding:4px 8px;border-radius:var(--eds-radius-md)}.eds-topbar-user__avatar{width:28px;height:28px;border-radius:50%;background:var(--gray-200);color:var(--gray-700);font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}.eds-topbar-user__email{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)}.eds-topbar-logout{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border-default);background:var(--surface-l0);color:var(--text-secondary);border-radius:var(--eds-radius-md);padding:6px 10px;cursor:pointer;font:inherit}.eds-topbar-logout:hover{background:var(--surface-l1);color:var(--text-primary);border-color:var(--gray-300)}@media(max-width:1100px){.eds-topbar-user__email{display:none}}\n"] }]
2552
- }], ctorParameters: () => [] });
2553
-
2554
- class EdsAppShellComponent {
2555
- constructor() {
2556
- /** EHR embed: false (default). Standalone Voice Hub: set showModuleNav: true. */
2557
- this.showModuleNav = !!inject(VOICE_RECEPTIONIST_CONFIG).showModuleNav;
2558
- }
2559
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2560
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsAppShellComponent, isStandalone: true, selector: "eds-app-shell", ngImport: i0, template: "<div class=\"eds-layout\">\n @if (showModuleNav) {\n <eds-module-nav />\n }\n <div class=\"eds-main\">\n <eds-topbar />\n <div class=\"eds-content\">\n <router-outlet />\n </div>\n </div>\n</div>\n<app-toast-container />\n", styles: [":host{display:block}.eds-layout{display:flex;min-height:100vh;background:var(--surface-l1)}.eds-main{flex:1;min-width:0;display:flex;flex-direction:column}.eds-content{flex:1;overflow:auto}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: EdsModuleNavComponent, selector: "eds-module-nav" }, { kind: "component", type: EdsTopbarComponent, selector: "eds-topbar" }, { kind: "component", type: ToastContainerComponent, selector: "app-toast-container" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2561
- }
2562
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, decorators: [{
2563
- type: Component,
2564
- args: [{ selector: 'eds-app-shell', imports: [
2565
- RouterOutlet,
2566
- EdsModuleNavComponent,
2567
- EdsTopbarComponent,
2568
- ToastContainerComponent,
2569
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-layout\">\n @if (showModuleNav) {\n <eds-module-nav />\n }\n <div class=\"eds-main\">\n <eds-topbar />\n <div class=\"eds-content\">\n <router-outlet />\n </div>\n </div>\n</div>\n<app-toast-container />\n", styles: [":host{display:block}.eds-layout{display:flex;min-height:100vh;background:var(--surface-l1)}.eds-main{flex:1;min-width:0;display:flex;flex-direction:column}.eds-content{flex:1;overflow:auto}\n"] }]
2570
- }] });
2571
-
2572
- /** Feature child routes (under the embed shell). */
2573
- const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
2574
- { path: '', pathMatch: 'full', redirectTo: 'dashboard' },
2575
- {
2576
- path: 'dashboard',
2577
- loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs').then((m) => m.DashboardPageComponent),
2578
- },
2579
- {
2580
- path: 'live',
2581
- loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs').then((m) => m.LiveCallsPageComponent),
2582
- },
2583
- {
2584
- path: 'live/:id',
2585
- loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs').then((m) => m.LiveMonitorPageComponent),
2586
- },
2587
- {
2588
- path: 'history',
2589
- loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs').then((m) => m.CallHistoryPageComponent),
2590
- },
2591
- {
2592
- path: 'history/:id',
2593
- loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs').then((m) => m.CallHistoryDetailPageComponent),
2594
- },
2595
- {
2596
- path: 'tasks',
2597
- loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs').then((m) => m.TasksPageComponent),
2598
- },
2599
- {
2600
- path: 'analytics',
2601
- loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs').then((m) => m.AnalyticsPageComponent),
2602
- },
2603
- {
2604
- path: 'settings',
2605
- loadComponent: () => import('./edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs').then((m) => m.SettingsPageComponent),
2606
- },
2607
- {
2608
- path: 'settings/outbound-purposes',
2609
- loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs').then((m) => m.OutboundPurposesPageComponent),
2610
- },
2611
- {
2612
- path: 'knowledge',
2613
- loadComponent: () => import('./edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs').then((m) => m.KnowledgeHubPageComponent),
2614
- },
2615
- {
2616
- path: 'audit',
2617
- loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs').then((m) => m.AuditPageComponent),
2618
- },
2619
- {
2620
- path: 'wizard',
2621
- loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs').then((m) => m.WizardPageComponent),
2622
- },
2623
- ];
2624
- /**
2625
- * Lazy-loadable routes for EHR (and standalone hosts).
2626
- * Mount under a host path, e.g. `{ path: 'ai-receptionist', children: VOICE_RECEPTIONIST_ROUTES }`.
2627
- */
2628
- const VOICE_RECEPTIONIST_ROUTES = [
2629
- {
2630
- path: '',
2631
- component: EdsAppShellComponent,
2632
- children: VOICE_RECEPTIONIST_FEATURE_ROUTES,
2633
- },
2634
- ];
2635
-
2636
- class EdsBadgeComponent {
2637
- constructor() {
2638
- this.tone = input('gray');
2639
- }
2640
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2641
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.17", type: EdsBadgeComponent, isStandalone: true, selector: "eds-badge", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `<span class="eds-badge" [attr.data-tone]="tone()"><ng-content /></span>`, isInline: true, styles: [".eds-badge{display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border-radius:var(--eds-radius-sm);font-family:var(--font-inter);font-size:11px;font-weight:600;line-height:1.4;white-space:nowrap}.eds-badge[data-tone=brand]{background:#3681f41f;color:var(--brand-600)}.eds-badge[data-tone=pos]{background:#0abf521f;color:var(--green-500)}.eds-badge[data-tone=notice]{background:#ffb90026;color:#b45309}.eds-badge[data-tone=neg]{background:#f037401f;color:var(--red-500)}.eds-badge[data-tone=violet]{background:#8e51ff1f;color:var(--violet-500)}.eds-badge[data-tone=gray]{background:var(--gray-100);color:var(--gray-600)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2642
- }
2643
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, decorators: [{
2644
- type: Component,
2645
- args: [{ selector: 'eds-badge', standalone: true, template: `<span class="eds-badge" [attr.data-tone]="tone()"><ng-content /></span>`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".eds-badge{display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border-radius:var(--eds-radius-sm);font-family:var(--font-inter);font-size:11px;font-weight:600;line-height:1.4;white-space:nowrap}.eds-badge[data-tone=brand]{background:#3681f41f;color:var(--brand-600)}.eds-badge[data-tone=pos]{background:#0abf521f;color:var(--green-500)}.eds-badge[data-tone=notice]{background:#ffb90026;color:#b45309}.eds-badge[data-tone=neg]{background:#f037401f;color:var(--red-500)}.eds-badge[data-tone=violet]{background:#8e51ff1f;color:var(--violet-500)}.eds-badge[data-tone=gray]{background:var(--gray-100);color:var(--gray-600)}\n"] }]
2646
- }] });
2647
-
2648
- /*
2649
- * Public API Surface of @edvak99/voice-receptionist-widget
2650
- */
2651
-
2652
- /**
2653
- * Generated bundle index. Do not edit.
2654
- */
2655
-
2656
- export { AiReceptionistApiService as A, DEFAULT_PLACEHOLDERS as D, EdsButtonComponent as E, ModalComponent as M, ToastService as T, VOICE_RECEPTIONIST_CONFIG as V, WELCOME_MESSAGE_MAX as W, EdsBadgeComponent as a, EdsIconComponent as b, EdsPulseDotComponent as c, resolveToolDisplayName as d, createVoiceReceptionistPaths as e, buildAutoWelcomeTemplate as f, resolveWelcomePreview as g, EdsAppShellComponent as h, VOICE_RECEPTIONIST_FEATURE_ROUTES as i, VOICE_RECEPTIONIST_ROUTES as j, VOICE_RECEPTIONIST_SESSION as k, provideVoiceReceptionist as p, resolveToolKey as r, toolEventTrackKey as t };
2657
- //# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs.map
3428
+ export { AiReceptionistApiService as A, DEFAULT_PAGE_SIZE as D, WELCOME_MESSAGE_MAX as W, historyResolutionLabel as a, historyResolution as b, callerMetaLine as c, ageFromDob as d, resolveToolKey as e, resolveToolDisplayName as f, toolEventTrackKey as g, historyResultChips as h, isUnknownCaller as i, createVoiceReceptionistPaths as j, totalPages as k, activityTypeLabel as l, initialsFromName as m, buildAutoWelcomeTemplate as n, resolveWelcomePreview as o, DEFAULT_PLACEHOLDERS as p, resultChipIcon as r, toE164Phone as t };
3429
+ //# sourceMappingURL=edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map