@edvak99/voice-receptionist-widget 1.1.3 → 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.
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +307 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs} +1275 -561
- package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +278 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +418 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +194 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-B8_qp7Fe.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs} +3 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-B8_qp7Fe.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cj2XTeK4.mjs → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cj2XTeK4.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs +21 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +440 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-eds-elapsed.pipe-BgLsnq6A.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs} +5 -21
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs +38 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +27 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs +22 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-list.service-0kGWO6sh.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs} +15 -5
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +203 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +81 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +1070 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +48 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs +127 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +268 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +968 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-DXxjMVUv.mjs → edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs} +56 -5
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C1Z6yC0q.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs} +4 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C1Z6yC0q.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +9 -6
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +8 -8
- package/lib/components/knowledge-test-modal/knowledge-test-modal.component.d.ts +28 -0
- package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +7 -4
- package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +26 -0
- package/lib/layout/eds-module-nav.component.d.ts +0 -3
- package/lib/mocks/agent-activity.seed.d.ts +2 -0
- package/lib/models/agent-activity.types.d.ts +54 -0
- package/lib/models/ai-receptionist.types.d.ts +25 -0
- package/lib/models/api-dto.types.d.ts +18 -0
- package/lib/models/live-transcript.types.d.ts +2 -1
- package/lib/models/outbound-patient.types.d.ts +23 -0
- package/lib/models/outbound-purpose.types.d.ts +2 -0
- package/lib/models/staff-media.types.d.ts +36 -0
- package/lib/pages/activities-page/activities-page.component.d.ts +57 -0
- package/lib/pages/analytics-page/analytics-page.component.d.ts +38 -14
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +2 -3
- package/lib/pages/call-history-page/call-history-page.component.d.ts +9 -6
- package/lib/pages/dashboard-page/dashboard-page.component.d.ts +12 -8
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +14 -24
- package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +34 -1
- package/lib/pages/settings-page/settings-fields.config.d.ts +16 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +48 -14
- package/lib/services/ai-receptionist-api.mock.d.ts +34 -0
- package/lib/services/ai-receptionist-api.service.d.ts +56 -1
- package/lib/services/live-transcript.service.d.ts +1 -0
- package/lib/services/staff-media.service.d.ts +52 -0
- package/lib/ui/eds-avatar.component.d.ts +5 -2
- package/lib/ui/eds-button.component.d.ts +1 -1
- package/lib/utils/call-history-display.util.d.ts +21 -0
- package/lib/utils/outbound-purpose.util.d.ts +19 -0
- package/lib/utils/transcript-timing.util.d.ts +1 -1
- package/lib/utils/welcome-message.util.d.ts +1 -1
- package/package.json +2 -1
- package/styles/eds.scss +95 -66
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Dsujn5az.mjs +0 -113
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Dsujn5az.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-xoLlHj8K.mjs +0 -67
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-xoLlHj8K.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-2K9W77As.mjs +0 -316
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-2K9W77As.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B5AnEjoh.mjs +0 -174
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B5AnEjoh.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-BgsBCKgZ.mjs +0 -70
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-BgsBCKgZ.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-BNdnbAVM.mjs +0 -248
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-BNdnbAVM.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CbphhP6o.mjs +0 -27
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CbphhP6o.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BgLsnq6A.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs +0 -609
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-0kGWO6sh.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DfLmK89M.mjs +0 -115
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DfLmK89M.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DNpLFqBw.mjs +0 -574
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DNpLFqBw.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +0 -54
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CYSZ832p.mjs +0 -265
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CYSZ832p.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-B9nv6wtB.mjs +0 -509
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-B9nv6wtB.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +0 -52
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DXxjMVUv.mjs.map +0 -1
- package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +0 -22
- package/lib/knowledge/components/faq-form/faq-form.component.d.ts +0 -27
- package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +0 -17
- package/lib/knowledge/mocks/practice-faq.seed.d.ts +0 -2
- package/lib/knowledge/models/practice-faq.model.d.ts +0 -50
- package/lib/knowledge/pages/knowledge-hub.page.d.ts +0 -54
- package/lib/knowledge/services/practice-faq-api.mock.d.ts +0 -16
- package/lib/knowledge/services/practice-faq-api.service.d.ts +0 -26
- package/lib/knowledge/utils/faq.util.d.ts +0 -3
- package/lib/layout/eds-topbar.component.d.ts +0 -29
- package/lib/pages/audit-page/audit-page.component.d.ts +0 -21
- 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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(--eds-color-surface);border:1px solid var(--eds-color-border);border-radius:var(--eds-radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--eds-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(--eds-color-primary-soft);color:var(--eds-color-primary)}.toast-success .toast-icon{background:var(--eds-color-success-soft);color:var(--eds-color-success)}.toast-error .toast-icon{background:var(--eds-color-danger-soft);color:var(--eds-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(--eds-color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--eds-color-surface-alt);color:var(--eds-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(--eds-color-surface);border:1px solid var(--eds-color-border);border-radius:var(--eds-radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--eds-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(--eds-color-primary-soft);color:var(--eds-color-primary)}.toast-success .toast-icon{background:var(--eds-color-success-soft);color:var(--eds-color-success)}.toast-error .toast-icon{background:var(--eds-color-danger-soft);color:var(--eds-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(--eds-color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--eds-color-surface-alt);color:var(--eds-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,6 +34,356 @@ function extractApiRows(body) {
|
|
|
87
34
|
return [];
|
|
88
35
|
}
|
|
89
36
|
|
|
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
|
+
|
|
90
387
|
const DEFAULT_PAGE_SIZE = 50;
|
|
91
388
|
/** Parse `{ items, total, page, limit }` or a bare array (legacy). */
|
|
92
389
|
function asPaginatedResult(body, fallbackPage = 1, fallbackLimit = DEFAULT_PAGE_SIZE) {
|
|
@@ -113,7 +410,7 @@ function totalPages(total, pageSize) {
|
|
|
113
410
|
return Math.max(1, Math.ceil(total / pageSize));
|
|
114
411
|
}
|
|
115
412
|
|
|
116
|
-
const WELCOME_MESSAGE_MAX =
|
|
413
|
+
const WELCOME_MESSAGE_MAX = 1000;
|
|
117
414
|
function buildAutoWelcomeTemplate(practiceName, agentName) {
|
|
118
415
|
const practice = practiceName.trim() || 'our office';
|
|
119
416
|
const agent = agentName.trim() || 'your AI receptionist';
|
|
@@ -133,6 +430,112 @@ function computeEffectiveWelcomeMessage(settings) {
|
|
|
133
430
|
return buildAutoWelcomeTemplate(String(general['practiceName'] ?? ''), String(general['agentName'] ?? ''));
|
|
134
431
|
}
|
|
135
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
|
+
|
|
136
539
|
/** V3 agent tool labels — mirrors GET /api/agent-tools/catalog */
|
|
137
540
|
const AGENT_TOOL_LABELS = {
|
|
138
541
|
find_doctors: 'Found Doctors',
|
|
@@ -239,6 +642,22 @@ function mapCallStatus(raw) {
|
|
|
239
642
|
];
|
|
240
643
|
return allowed.includes(s) ? s : 'ai';
|
|
241
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
|
+
}
|
|
242
661
|
function mapSentiment(raw) {
|
|
243
662
|
const s = String(raw ?? 'neutral').toLowerCase();
|
|
244
663
|
if (s === 'calm' || s === 'neutral' || s === 'frustrated' || s === 'urgent')
|
|
@@ -283,10 +702,19 @@ function mapRecordingMeta(dto) {
|
|
|
283
702
|
};
|
|
284
703
|
}
|
|
285
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;
|
|
286
714
|
return {
|
|
287
715
|
id: dto.id,
|
|
288
716
|
name: dto.name,
|
|
289
|
-
initials: dto.initials,
|
|
717
|
+
initials: dto.initials || (dto.matched ? '' : '?'),
|
|
290
718
|
avatar: dto.avatar,
|
|
291
719
|
phone: dto.phone,
|
|
292
720
|
verification: mapVerification(dto.verification),
|
|
@@ -297,22 +725,48 @@ function mapLiveCallDto(dto) {
|
|
|
297
725
|
sentiment: mapSentiment(dto.sentiment),
|
|
298
726
|
confidence: normalizeConfidence(dto.confidence),
|
|
299
727
|
goal: dto.goal,
|
|
300
|
-
dob
|
|
728
|
+
dob,
|
|
729
|
+
age,
|
|
730
|
+
gender,
|
|
301
731
|
location: dto.location ?? '',
|
|
302
732
|
provider: dto.provider,
|
|
303
733
|
language: dto.language,
|
|
304
734
|
matched: dto.matched,
|
|
305
735
|
assigned: dto.assigned,
|
|
736
|
+
controlMode: mapControlMode(dto.controlMode, dto.status, dto.aiPaused),
|
|
306
737
|
tools: (dto.tools ?? []).map(mapToolEvent),
|
|
307
738
|
transcript: (dto.transcript ?? []).map(mapTranscriptLine),
|
|
739
|
+
aiPaused: dto.aiPaused === true || String(dto.controlMode ?? '').toLowerCase() === 'paused',
|
|
740
|
+
aiHandedOff: dto.aiHandedOff === true,
|
|
308
741
|
};
|
|
309
742
|
}
|
|
310
743
|
function mapHistoryCallDto(dto) {
|
|
311
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
|
+
}
|
|
312
766
|
return {
|
|
313
767
|
id: dto.id,
|
|
314
768
|
name: dto.name,
|
|
315
|
-
initials: dto.initials,
|
|
769
|
+
initials: dto.initials || '?',
|
|
316
770
|
phone: dto.phone,
|
|
317
771
|
direction: dto.dir,
|
|
318
772
|
outcome: dto.outcome,
|
|
@@ -324,6 +778,14 @@ function mapHistoryCallDto(dto) {
|
|
|
324
778
|
location: dto.location,
|
|
325
779
|
verification: mapVerification(dto.verify),
|
|
326
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,
|
|
327
789
|
};
|
|
328
790
|
}
|
|
329
791
|
function mapHistoryDetailDto(call, detail, recordingUrl) {
|
|
@@ -518,6 +980,8 @@ function mapVolumeSeries(raw) {
|
|
|
518
980
|
day: p.day,
|
|
519
981
|
label: p.label,
|
|
520
982
|
count: p.count,
|
|
983
|
+
inbound: p.inbound,
|
|
984
|
+
outbound: p.outbound,
|
|
521
985
|
})),
|
|
522
986
|
};
|
|
523
987
|
}
|
|
@@ -595,22 +1059,77 @@ function mapSettingsResponse(body, safetyBody) {
|
|
|
595
1059
|
status: c.ok ? 'ok' : 'warn',
|
|
596
1060
|
}));
|
|
597
1061
|
const mapped = {
|
|
598
|
-
general: section('general'),
|
|
599
|
-
phone: section('phoneAvailability'),
|
|
1062
|
+
general: normalizeGeneralSection(section('general')),
|
|
1063
|
+
phone: normalizePhoneSection(section('phoneAvailability')),
|
|
600
1064
|
tasks: section('allowedTasks'),
|
|
601
1065
|
scheduling: section('schedulingRules'),
|
|
602
1066
|
refill: section('refillRules'),
|
|
603
1067
|
verify: section('patientVerification'),
|
|
604
1068
|
escalation: section('humanEscalation'),
|
|
605
1069
|
outbound: section('outboundCalling'),
|
|
1070
|
+
knowledge: normalizeKnowledgeSection(section('knowledgeBase')),
|
|
606
1071
|
safety,
|
|
607
|
-
transcripts: section('transcriptsRecording'),
|
|
1072
|
+
transcripts: normalizeTranscriptsSection(section('transcriptsRecording')),
|
|
608
1073
|
advanced: section('advanced'),
|
|
609
1074
|
};
|
|
610
1075
|
mapped.effectiveWelcomeMessage =
|
|
611
1076
|
raw.effectiveWelcomeMessage ?? computeEffectiveWelcomeMessage(mapped);
|
|
1077
|
+
if (!mapped.knowledge) {
|
|
1078
|
+
mapped.knowledge = { whatToSay: '', whatNotToSay: '' };
|
|
1079
|
+
}
|
|
612
1080
|
return mapped;
|
|
613
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
|
+
}
|
|
614
1133
|
function humanizeSettingsKey(key) {
|
|
615
1134
|
return key
|
|
616
1135
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -629,6 +1148,7 @@ function mapSettingsToRequest(settings) {
|
|
|
629
1148
|
patientVerification: settings.verify,
|
|
630
1149
|
humanEscalation: settings.escalation,
|
|
631
1150
|
outboundCalling: settings.outbound,
|
|
1151
|
+
knowledgeBase: settings.knowledge,
|
|
632
1152
|
transcriptsRecording: settings.transcripts,
|
|
633
1153
|
advanced: settings.advanced,
|
|
634
1154
|
};
|
|
@@ -688,7 +1208,7 @@ function asAttentionList(body) {
|
|
|
688
1208
|
return [];
|
|
689
1209
|
}
|
|
690
1210
|
|
|
691
|
-
const now$
|
|
1211
|
+
const now$2 = Date.now();
|
|
692
1212
|
function item(partial) {
|
|
693
1213
|
return {
|
|
694
1214
|
callId: 'LC-4821',
|
|
@@ -699,7 +1219,7 @@ function item(partial) {
|
|
|
699
1219
|
durationMs: 340,
|
|
700
1220
|
toolName: null,
|
|
701
1221
|
errorMessage: null,
|
|
702
|
-
startedAt: new Date(now$
|
|
1222
|
+
startedAt: new Date(now$2 - 120_000).toISOString(),
|
|
703
1223
|
requestPreview: '{"phone":"+15551234567"}',
|
|
704
1224
|
responsePreview: '{"items":[{"id":"p1"}]}',
|
|
705
1225
|
...partial,
|
|
@@ -715,7 +1235,7 @@ const SEED_CALL_API_LOGS = {
|
|
|
715
1235
|
urlPath: '/vonage/answer',
|
|
716
1236
|
statusCode: 200,
|
|
717
1237
|
durationMs: 120,
|
|
718
|
-
startedAt: new Date(now$
|
|
1238
|
+
startedAt: new Date(now$2 - 300_000).toISOString(),
|
|
719
1239
|
requestPreview: '{"callId":"..."}',
|
|
720
1240
|
responsePreview: '{"status":"connected"}',
|
|
721
1241
|
}),
|
|
@@ -728,7 +1248,7 @@ const SEED_CALL_API_LOGS = {
|
|
|
728
1248
|
urlPath: '/api/v1/patients',
|
|
729
1249
|
statusCode: 200,
|
|
730
1250
|
durationMs: 420,
|
|
731
|
-
startedAt: new Date(now$
|
|
1251
|
+
startedAt: new Date(now$2 - 240_000).toISOString(),
|
|
732
1252
|
}),
|
|
733
1253
|
item({
|
|
734
1254
|
id: 'log-3',
|
|
@@ -738,7 +1258,7 @@ const SEED_CALL_API_LOGS = {
|
|
|
738
1258
|
urlPath: '/v1/chat/completions',
|
|
739
1259
|
statusCode: 200,
|
|
740
1260
|
durationMs: 890,
|
|
741
|
-
startedAt: new Date(now$
|
|
1261
|
+
startedAt: new Date(now$2 - 180_000).toISOString(),
|
|
742
1262
|
requestPreview: '{"model":"gpt-4o-mini"}',
|
|
743
1263
|
responsePreview: '{"choices":[...]}',
|
|
744
1264
|
}),
|
|
@@ -751,7 +1271,7 @@ const SEED_CALL_API_LOGS = {
|
|
|
751
1271
|
urlPath: '/api/appointments',
|
|
752
1272
|
statusCode: 201,
|
|
753
1273
|
durationMs: 560,
|
|
754
|
-
startedAt: new Date(now$
|
|
1274
|
+
startedAt: new Date(now$2 - 60_000).toISOString(),
|
|
755
1275
|
}),
|
|
756
1276
|
],
|
|
757
1277
|
'HC-9001': [
|
|
@@ -765,7 +1285,7 @@ const SEED_CALL_API_LOGS = {
|
|
|
765
1285
|
success: false,
|
|
766
1286
|
durationMs: 2100,
|
|
767
1287
|
errorMessage: 'Internal Server Error',
|
|
768
|
-
startedAt: new Date(now$
|
|
1288
|
+
startedAt: new Date(now$2 - 86_400_000).toISOString(),
|
|
769
1289
|
}),
|
|
770
1290
|
],
|
|
771
1291
|
};
|
|
@@ -776,7 +1296,7 @@ const SEED_API_LOG_DETAILS = {
|
|
|
776
1296
|
requestBody: { patientId: 'p1', slotId: 'slot-9', reason: 'Annual checkup' },
|
|
777
1297
|
responseBody: { appointmentId: 'apt-42', status: 'confirmed' },
|
|
778
1298
|
metadata: { practiceId: 'prac-1', locationId: 'loc-1' },
|
|
779
|
-
completedAt: new Date(now$
|
|
1299
|
+
completedAt: new Date(now$2 - 59_500).toISOString(),
|
|
780
1300
|
},
|
|
781
1301
|
'log-f1': {
|
|
782
1302
|
...SEED_CALL_API_LOGS['HC-9001'][0],
|
|
@@ -784,7 +1304,7 @@ const SEED_API_LOG_DETAILS = {
|
|
|
784
1304
|
requestBody: { phone: '+15559876543' },
|
|
785
1305
|
responseBody: { error: 'Internal Server Error', message: 'Database timeout' },
|
|
786
1306
|
metadata: { retryCount: 1 },
|
|
787
|
-
completedAt: new Date(now$
|
|
1307
|
+
completedAt: new Date(now$2 - 86_398_000).toISOString(),
|
|
788
1308
|
},
|
|
789
1309
|
};
|
|
790
1310
|
function allSeedLogs() {
|
|
@@ -808,7 +1328,7 @@ const SEED_API_LOG_STATS = {
|
|
|
808
1328
|
success: false,
|
|
809
1329
|
durationMs: 2100,
|
|
810
1330
|
errorMessage: 'Internal Server Error',
|
|
811
|
-
startedAt: new Date(now$
|
|
1331
|
+
startedAt: new Date(now$2 - 3_600_000).toISOString(),
|
|
812
1332
|
}),
|
|
813
1333
|
item({
|
|
814
1334
|
id: 'log-fail-2',
|
|
@@ -819,7 +1339,7 @@ const SEED_API_LOG_STATS = {
|
|
|
819
1339
|
success: false,
|
|
820
1340
|
durationMs: 1500,
|
|
821
1341
|
errorMessage: 'Service unavailable',
|
|
822
|
-
startedAt: new Date(now$
|
|
1342
|
+
startedAt: new Date(now$2 - 7_200_000).toISOString(),
|
|
823
1343
|
}),
|
|
824
1344
|
item({
|
|
825
1345
|
id: 'log-fail-3',
|
|
@@ -830,13 +1350,13 @@ const SEED_API_LOG_STATS = {
|
|
|
830
1350
|
success: false,
|
|
831
1351
|
durationMs: 980,
|
|
832
1352
|
errorMessage: 'Gateway timeout',
|
|
833
|
-
startedAt: new Date(now$
|
|
1353
|
+
startedAt: new Date(now$2 - 10_800_000).toISOString(),
|
|
834
1354
|
}),
|
|
835
1355
|
],
|
|
836
1356
|
};
|
|
837
1357
|
|
|
838
|
-
const now = Date.now();
|
|
839
|
-
const min = (m) => now - m * 60_000;
|
|
1358
|
+
const now$1 = Date.now();
|
|
1359
|
+
const min = (m) => now$1 - m * 60_000;
|
|
840
1360
|
const baseTranscript = [
|
|
841
1361
|
{ who: 'system', text: 'Call connected · inbound PSTN' },
|
|
842
1362
|
{
|
|
@@ -875,6 +1395,8 @@ const SEED_LIVE_CALLS = [
|
|
|
875
1395
|
confidence: 92,
|
|
876
1396
|
goal: 'Reschedule visit with Dr. Chen',
|
|
877
1397
|
dob: '1985-03-14',
|
|
1398
|
+
age: 41,
|
|
1399
|
+
gender: 'F',
|
|
878
1400
|
location: 'Riverside Main',
|
|
879
1401
|
provider: 'Dr. Elena Chen',
|
|
880
1402
|
language: 'English',
|
|
@@ -922,6 +1444,9 @@ const SEED_LIVE_CALLS = [
|
|
|
922
1444
|
sentiment: 'frustrated',
|
|
923
1445
|
confidence: 71,
|
|
924
1446
|
goal: 'Refill lisinopril 10mg',
|
|
1447
|
+
dob: '1978-09-22',
|
|
1448
|
+
age: 47,
|
|
1449
|
+
gender: 'M',
|
|
925
1450
|
location: 'North Clinic',
|
|
926
1451
|
provider: 'Dr. Patel',
|
|
927
1452
|
language: 'English',
|
|
@@ -1018,59 +1543,81 @@ const SEED_ATTENTION = [
|
|
|
1018
1543
|
const SEED_HISTORY = [
|
|
1019
1544
|
{
|
|
1020
1545
|
id: 'HC-9001',
|
|
1021
|
-
name: '
|
|
1022
|
-
initials: '
|
|
1546
|
+
name: 'Sooraj Jagadeesh',
|
|
1547
|
+
initials: 'SJ',
|
|
1023
1548
|
phone: '+1 (512) 555-0211',
|
|
1024
1549
|
direction: 'inbound',
|
|
1025
1550
|
outcome: 'Appointment booked',
|
|
1026
|
-
reason: '
|
|
1027
|
-
duration:
|
|
1028
|
-
|
|
1551
|
+
reason: 'Appointment Refill',
|
|
1552
|
+
duration: 252,
|
|
1553
|
+
durationLabel: '4:12',
|
|
1554
|
+
started: Date.parse('2026-06-12T14:20:00'),
|
|
1029
1555
|
location: 'Riverside Main',
|
|
1030
1556
|
verification: 'verified',
|
|
1031
1557
|
review: 'reviewed',
|
|
1558
|
+
handledBy: 'ai',
|
|
1559
|
+
dob: '2002-06-30',
|
|
1560
|
+
age: 23,
|
|
1561
|
+
gender: 'M',
|
|
1562
|
+
results: ['Appointment Booked'],
|
|
1032
1563
|
},
|
|
1033
1564
|
{
|
|
1034
1565
|
id: 'HC-9002',
|
|
1035
|
-
name: '
|
|
1036
|
-
initials: '
|
|
1037
|
-
phone: '+1 (
|
|
1566
|
+
name: 'Unknown Caller',
|
|
1567
|
+
initials: '?',
|
|
1568
|
+
phone: '+1 (555) 010-2299',
|
|
1038
1569
|
direction: 'outbound',
|
|
1039
|
-
outcome: '
|
|
1040
|
-
reason: '
|
|
1041
|
-
duration:
|
|
1042
|
-
|
|
1570
|
+
outcome: 'Task created',
|
|
1571
|
+
reason: 'Appointment',
|
|
1572
|
+
duration: 165,
|
|
1573
|
+
durationLabel: '2:45',
|
|
1574
|
+
started: Date.parse('2026-06-12T11:05:00'),
|
|
1043
1575
|
location: 'North Clinic',
|
|
1044
|
-
verification: '
|
|
1576
|
+
verification: 'unknown',
|
|
1045
1577
|
review: 'needs',
|
|
1578
|
+
handledBy: 'ai',
|
|
1579
|
+
results: ['Task Created'],
|
|
1580
|
+
risk: 'review',
|
|
1046
1581
|
},
|
|
1047
1582
|
{
|
|
1048
1583
|
id: 'HC-9003',
|
|
1049
|
-
name: '
|
|
1050
|
-
initials: '
|
|
1584
|
+
name: 'Dinesh Kumar',
|
|
1585
|
+
initials: 'DK',
|
|
1051
1586
|
phone: '+1 (512) 555-0444',
|
|
1052
1587
|
direction: 'inbound',
|
|
1053
|
-
outcome: '
|
|
1054
|
-
reason: '
|
|
1055
|
-
duration:
|
|
1056
|
-
|
|
1588
|
+
outcome: 'Completed',
|
|
1589
|
+
reason: 'Appointment',
|
|
1590
|
+
duration: 372,
|
|
1591
|
+
durationLabel: '6:12',
|
|
1592
|
+
started: Date.parse('2026-06-11T16:45:00'),
|
|
1057
1593
|
location: 'Riverside Main',
|
|
1058
1594
|
verification: 'verified',
|
|
1059
|
-
review: '
|
|
1595
|
+
review: 'reviewed',
|
|
1596
|
+
handledBy: 'staff',
|
|
1597
|
+
dob: '1988-03-14',
|
|
1598
|
+
age: 38,
|
|
1599
|
+
gender: 'M',
|
|
1600
|
+
results: ['Patient Registered', 'Appointment Booked'],
|
|
1060
1601
|
},
|
|
1061
1602
|
{
|
|
1062
1603
|
id: 'HC-9004',
|
|
1063
|
-
name: '
|
|
1064
|
-
initials: '
|
|
1604
|
+
name: 'Meera Banerjee',
|
|
1605
|
+
initials: 'MB',
|
|
1065
1606
|
phone: '+1 (512) 555-0555',
|
|
1066
1607
|
direction: 'inbound',
|
|
1067
|
-
outcome: '
|
|
1068
|
-
reason: '
|
|
1069
|
-
duration:
|
|
1070
|
-
|
|
1608
|
+
outcome: 'Completed',
|
|
1609
|
+
reason: 'Information',
|
|
1610
|
+
duration: 93,
|
|
1611
|
+
durationLabel: '1:33',
|
|
1612
|
+
started: Date.parse('2026-06-11T09:12:00'),
|
|
1071
1613
|
location: 'South Clinic',
|
|
1072
1614
|
verification: 'partial',
|
|
1073
|
-
review: '
|
|
1615
|
+
review: 'needs',
|
|
1616
|
+
handledBy: 'ai',
|
|
1617
|
+
dob: '1995-11-02',
|
|
1618
|
+
age: 30,
|
|
1619
|
+
gender: 'F',
|
|
1620
|
+
results: ['Information Provided'],
|
|
1074
1621
|
},
|
|
1075
1622
|
{
|
|
1076
1623
|
id: 'HC-9005',
|
|
@@ -1081,10 +1628,13 @@ const SEED_HISTORY = [
|
|
|
1081
1628
|
outcome: 'Completed',
|
|
1082
1629
|
reason: 'Hours & directions',
|
|
1083
1630
|
duration: 89,
|
|
1084
|
-
|
|
1631
|
+
durationLabel: '1:29',
|
|
1632
|
+
started: Date.parse('2026-06-10T13:40:00'),
|
|
1085
1633
|
location: 'North Clinic',
|
|
1086
1634
|
verification: 'unknown',
|
|
1087
1635
|
review: 'reviewed',
|
|
1636
|
+
handledBy: 'ai',
|
|
1637
|
+
results: ['Information Provided'],
|
|
1088
1638
|
},
|
|
1089
1639
|
];
|
|
1090
1640
|
const SEED_HISTORY_DETAILS = {
|
|
@@ -1218,57 +1768,95 @@ const SEED_AUDIT = [
|
|
|
1218
1768
|
const SEED_ANALYTICS = {
|
|
1219
1769
|
range: '30d',
|
|
1220
1770
|
kpiCards: [
|
|
1221
|
-
{
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
+
},
|
|
1225
1813
|
],
|
|
1226
1814
|
volume: {
|
|
1227
1815
|
timezone: 'America/Chicago',
|
|
1228
|
-
startDate: '2025-05-
|
|
1816
|
+
startDate: '2025-05-01',
|
|
1229
1817
|
endDate: '2025-05-28',
|
|
1230
1818
|
points: [
|
|
1231
|
-
{ date: '2025-05-
|
|
1232
|
-
{ date: '2025-05-
|
|
1233
|
-
{ date: '2025-05-
|
|
1234
|
-
{ date: '2025-05-
|
|
1235
|
-
{ date: '2025-05-26', day: 'Fri', label: 'Fri May 26', count: 47 },
|
|
1236
|
-
{ date: '2025-05-27', day: 'Sat', label: 'Sat May 27', count: 22 },
|
|
1237
|
-
{ 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 },
|
|
1238
1823
|
],
|
|
1239
1824
|
},
|
|
1240
1825
|
outcomes: [
|
|
1241
|
-
{ label: '
|
|
1242
|
-
{ label: '
|
|
1243
|
-
{ label: '
|
|
1244
|
-
{ 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' },
|
|
1245
1829
|
],
|
|
1246
1830
|
reasons: [
|
|
1247
|
-
{ label: '
|
|
1248
|
-
{ label: 'Refills', value:
|
|
1249
|
-
{ label: '
|
|
1250
|
-
{ label: '
|
|
1251
|
-
{ label: '
|
|
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 },
|
|
1252
1838
|
],
|
|
1253
1839
|
transfers: [
|
|
1254
|
-
{ label: '
|
|
1255
|
-
{ label: '
|
|
1256
|
-
{ label: '
|
|
1257
|
-
{ label: '
|
|
1840
|
+
{ label: 'Out of Scope', value: 68 },
|
|
1841
|
+
{ label: 'Multiple Requests', value: 24 },
|
|
1842
|
+
{ label: 'Identity Unresolved', value: 20 },
|
|
1843
|
+
{ label: 'Others', value: 23 },
|
|
1258
1844
|
],
|
|
1259
1845
|
peakHours: {
|
|
1260
1846
|
timezone: 'America/Chicago',
|
|
1261
1847
|
points: Array.from({ length: 24 }, (_, hour) => ({
|
|
1262
1848
|
hour,
|
|
1263
1849
|
label: hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`,
|
|
1264
|
-
count: hour >= 8 && hour <=
|
|
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),
|
|
1265
1853
|
})),
|
|
1266
1854
|
},
|
|
1267
1855
|
kpis: {
|
|
1268
|
-
totalCalls:
|
|
1856
|
+
totalCalls: 1234,
|
|
1269
1857
|
avgDuration: 186,
|
|
1270
|
-
bookingRate:
|
|
1271
|
-
transferRate:
|
|
1858
|
+
bookingRate: 89,
|
|
1859
|
+
transferRate: 4,
|
|
1272
1860
|
},
|
|
1273
1861
|
};
|
|
1274
1862
|
const SEED_SETTINGS = {
|
|
@@ -1276,26 +1864,50 @@ const SEED_SETTINGS = {
|
|
|
1276
1864
|
practiceName: 'Riverside Family Medicine',
|
|
1277
1865
|
timezone: 'America/Chicago',
|
|
1278
1866
|
language: 'English',
|
|
1279
|
-
|
|
1280
|
-
|
|
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?',
|
|
1281
1871
|
ttsVoiceId: '7EzWGsX10sAS4c9m9cPf',
|
|
1282
1872
|
},
|
|
1283
|
-
phone: {
|
|
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
|
+
},
|
|
1284
1885
|
tasks: { autoCreate: true, defaultAssignee: 'Front desk' },
|
|
1285
1886
|
scheduling: { leadTimeHours: 4, allowSameDay: true },
|
|
1286
1887
|
refill: { autoApproveDays: 7, requireStaffReview: true },
|
|
1287
1888
|
verify: { requireDob: true, requireName: true, maxAttempts: 3 },
|
|
1288
1889
|
escalation: { sentimentThreshold: 'frustrated', urgentKeywords: 'emergency, chest pain' },
|
|
1289
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
|
+
},
|
|
1290
1895
|
safety: [
|
|
1291
1896
|
{ id: 's1', label: 'HIPAA logging enabled', status: 'ok' },
|
|
1292
1897
|
{ id: 's2', label: 'Recording consent script', status: 'ok' },
|
|
1293
1898
|
{ id: 's3', label: 'Emergency routing tested', status: 'warn' },
|
|
1294
1899
|
{ id: 's4', label: 'PHI redaction in transcripts', status: 'ok' },
|
|
1295
1900
|
],
|
|
1296
|
-
transcripts: {
|
|
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
|
+
},
|
|
1297
1909
|
advanced: { modelVersion: 'edvak-v2', debugMode: false },
|
|
1298
|
-
effectiveWelcomeMessage: '
|
|
1910
|
+
effectiveWelcomeMessage: 'Thanks for calling Riverside Family Medicine! This is Robin, the practice’s virtual assistant. How can I help you today?',
|
|
1299
1911
|
};
|
|
1300
1912
|
const SEED_SETUP = {
|
|
1301
1913
|
readiness: 92,
|
|
@@ -1423,6 +2035,210 @@ const SEED_OUTBOUND_PURPOSES = [
|
|
|
1423
2035
|
},
|
|
1424
2036
|
];
|
|
1425
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
|
+
|
|
1426
2242
|
const MOCK_DELAY_MS = 200;
|
|
1427
2243
|
class AiReceptionistApiMockService {
|
|
1428
2244
|
constructor() {
|
|
@@ -1560,17 +2376,58 @@ class AiReceptionistApiMockService {
|
|
|
1560
2376
|
});
|
|
1561
2377
|
}
|
|
1562
2378
|
postTakeover(id) {
|
|
1563
|
-
this.
|
|
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
|
+
}));
|
|
1564
2411
|
return this.mock({ success: true });
|
|
1565
2412
|
}
|
|
1566
2413
|
postResumeAi(id) {
|
|
1567
|
-
this.
|
|
1568
|
-
return this.mock({ success: true });
|
|
2414
|
+
return this.postMediaLeave(id);
|
|
1569
2415
|
}
|
|
1570
2416
|
postPause(id) {
|
|
1571
|
-
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
|
+
}));
|
|
1572
2426
|
return this.mock({ success: true });
|
|
1573
2427
|
}
|
|
2428
|
+
postListen(id) {
|
|
2429
|
+
return this.postMediaJoin(id, 'listen').pipe(map(() => ({ data: { success: true } })));
|
|
2430
|
+
}
|
|
1574
2431
|
postTransfer(id) {
|
|
1575
2432
|
this.patchLive(id, (c) => ({ ...c, status: 'transferring' }));
|
|
1576
2433
|
return this.mock({ success: true });
|
|
@@ -1661,6 +2518,15 @@ class AiReceptionistApiMockService {
|
|
|
1661
2518
|
]);
|
|
1662
2519
|
return of(new Blob([silent], { type: 'audio/wav' })).pipe(delay(MOCK_DELAY_MS));
|
|
1663
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
|
+
}
|
|
1664
2530
|
withEffectiveWelcome(settings) {
|
|
1665
2531
|
return {
|
|
1666
2532
|
...settings,
|
|
@@ -1679,6 +2545,29 @@ class AiReceptionistApiMockService {
|
|
|
1679
2545
|
rows = rows.filter((e) => e.actor.toLowerCase().includes(actor.toLowerCase()));
|
|
1680
2546
|
return this.paginate(rows, opts?.page, opts?.limit);
|
|
1681
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);
|
|
2570
|
+
}
|
|
1682
2571
|
getSetup() {
|
|
1683
2572
|
return this.mock(SEED_SETUP);
|
|
1684
2573
|
}
|
|
@@ -1834,12 +2723,78 @@ class AiReceptionistApiMockService {
|
|
|
1834
2723
|
return of({ success: true }).pipe(delay(MOCK_DELAY_MS));
|
|
1835
2724
|
}
|
|
1836
2725
|
placeOutboundCall(body) {
|
|
2726
|
+
const idHint = body.appointmentId
|
|
2727
|
+
? ` (appt ${body.appointmentId})`
|
|
2728
|
+
: body.patientId
|
|
2729
|
+
? ` (patient ${body.patientId})`
|
|
2730
|
+
: '';
|
|
1837
2731
|
return of({
|
|
1838
2732
|
success: true,
|
|
1839
2733
|
callId: `LC-${Math.floor(4800 + Math.random() * 100)}`,
|
|
1840
|
-
message: `Outbound call started to ${body.phone}`,
|
|
2734
|
+
message: `Outbound call started to ${body.phone}${idHint}`,
|
|
1841
2735
|
}).pipe(delay(MOCK_DELAY_MS));
|
|
1842
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
|
+
}
|
|
1843
2798
|
patchLive(id, fn) {
|
|
1844
2799
|
this.liveCalls.update((calls) => calls.map((c) => (c.id === id ? fn(structuredClone(c)) : c)));
|
|
1845
2800
|
}
|
|
@@ -2025,10 +2980,49 @@ class AiReceptionistApiService {
|
|
|
2025
2980
|
status: err.status,
|
|
2026
2981
|
}))));
|
|
2027
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
|
+
*/
|
|
2028
3014
|
postTakeover(id) {
|
|
2029
3015
|
if (this.useMock)
|
|
2030
|
-
return this.mock.
|
|
2031
|
-
|
|
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')))));
|
|
2032
3026
|
}
|
|
2033
3027
|
postResumeAi(id) {
|
|
2034
3028
|
if (this.useMock)
|
|
@@ -2040,6 +3034,25 @@ class AiReceptionistApiService {
|
|
|
2040
3034
|
return this.mock.postPause(id);
|
|
2041
3035
|
return this.postCallAction(id, 'pause');
|
|
2042
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
|
+
}
|
|
2043
3056
|
postTransfer(id) {
|
|
2044
3057
|
if (this.useMock)
|
|
2045
3058
|
return this.mock.postTransfer(id);
|
|
@@ -2135,7 +3148,7 @@ class AiReceptionistApiService {
|
|
|
2135
3148
|
return this.mock.putSettings(next);
|
|
2136
3149
|
return this.http
|
|
2137
3150
|
.put(`${this.arBase}/settings`, { settings: mapSettingsToRequest(next) })
|
|
2138
|
-
.pipe(switchMap(() => this.getSettings()));
|
|
3151
|
+
.pipe(switchMap(() => this.getSettings()), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to save agent settings')))));
|
|
2139
3152
|
}
|
|
2140
3153
|
/** ElevenLabs spoken sample for Agent Settings voice picker. */
|
|
2141
3154
|
previewVoice(voiceId) {
|
|
@@ -2145,6 +3158,34 @@ class AiReceptionistApiService {
|
|
|
2145
3158
|
responseType: 'blob',
|
|
2146
3159
|
});
|
|
2147
3160
|
}
|
|
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
|
+
}
|
|
2148
3189
|
getAudit(q, opts) {
|
|
2149
3190
|
if (this.useMock)
|
|
2150
3191
|
return this.mock.getAudit(q, undefined, opts);
|
|
@@ -2161,6 +3202,34 @@ class AiReceptionistApiService {
|
|
|
2161
3202
|
};
|
|
2162
3203
|
}));
|
|
2163
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
|
+
}));
|
|
3232
|
+
}
|
|
2164
3233
|
getSetup() {
|
|
2165
3234
|
if (this.useMock)
|
|
2166
3235
|
return this.mock.getSetup();
|
|
@@ -2277,6 +3346,62 @@ class AiReceptionistApiService {
|
|
|
2277
3346
|
return throwError(() => ({ message: detail || 'Failed to place outbound call.' }));
|
|
2278
3347
|
}));
|
|
2279
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
|
+
}
|
|
2280
3405
|
postCallAction(id, action) {
|
|
2281
3406
|
const enc = encodeURIComponent(id);
|
|
2282
3407
|
return this.http
|
|
@@ -2300,416 +3425,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2300
3425
|
args: [{ providedIn: 'root' }]
|
|
2301
3426
|
}] });
|
|
2302
3427
|
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
* Always re-fetches GET /status after mutations.
|
|
2306
|
-
*/
|
|
2307
|
-
class AgentOnlineService {
|
|
2308
|
-
constructor() {
|
|
2309
|
-
this.api = inject(AiReceptionistApiService);
|
|
2310
|
-
/** true = online, false = offline, null = unknown / not loaded */
|
|
2311
|
-
this._online = signal(null);
|
|
2312
|
-
this._loading = signal(false);
|
|
2313
|
-
this._toggling = signal(false);
|
|
2314
|
-
this._error = signal(null);
|
|
2315
|
-
this._readiness = signal(null);
|
|
2316
|
-
this.online = this._online.asReadonly();
|
|
2317
|
-
this.loading = this._loading.asReadonly();
|
|
2318
|
-
this.toggling = this._toggling.asReadonly();
|
|
2319
|
-
this.error = this._error.asReadonly();
|
|
2320
|
-
this.readiness = this._readiness.asReadonly();
|
|
2321
|
-
}
|
|
2322
|
-
refresh() {
|
|
2323
|
-
this._loading.set(true);
|
|
2324
|
-
this._error.set(null);
|
|
2325
|
-
this.api.getStatus().subscribe({
|
|
2326
|
-
next: ({ data }) => {
|
|
2327
|
-
this._online.set(data.agentOn);
|
|
2328
|
-
this._readiness.set(data.readiness);
|
|
2329
|
-
this._loading.set(false);
|
|
2330
|
-
},
|
|
2331
|
-
error: (err) => {
|
|
2332
|
-
this._online.set(null);
|
|
2333
|
-
this._error.set(err.message ?? 'Could not load agent status.');
|
|
2334
|
-
this._loading.set(false);
|
|
2335
|
-
},
|
|
2336
|
-
});
|
|
2337
|
-
}
|
|
2338
|
-
/**
|
|
2339
|
-
* PUT /agent-online then re-fetch GET /status.
|
|
2340
|
-
*/
|
|
2341
|
-
setOnline(enabled) {
|
|
2342
|
-
this._toggling.set(true);
|
|
2343
|
-
this._error.set(null);
|
|
2344
|
-
return this.api.setAgentOnline(enabled).pipe(switchMap(() => this.api.getStatus().pipe(catchError((err) => {
|
|
2345
|
-
this._error.set(err.message ?? 'Status refresh failed after toggle.');
|
|
2346
|
-
return of(null);
|
|
2347
|
-
}))), tap((result) => {
|
|
2348
|
-
if (result) {
|
|
2349
|
-
this._online.set(result.data.agentOn);
|
|
2350
|
-
this._readiness.set(result.data.readiness);
|
|
2351
|
-
}
|
|
2352
|
-
else {
|
|
2353
|
-
// Toggle likely succeeded; use requested value until refresh works.
|
|
2354
|
-
this._online.set(enabled);
|
|
2355
|
-
}
|
|
2356
|
-
}), catchError((err) => {
|
|
2357
|
-
this._error.set(err.message ?? 'Failed to update agent online status.');
|
|
2358
|
-
return throwError(() => err);
|
|
2359
|
-
}), finalize(() => this._toggling.set(false)));
|
|
2360
|
-
}
|
|
2361
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2362
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, providedIn: 'root' }); }
|
|
2363
|
-
}
|
|
2364
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, decorators: [{
|
|
2365
|
-
type: Injectable,
|
|
2366
|
-
args: [{ providedIn: 'root' }]
|
|
2367
|
-
}] });
|
|
2368
|
-
|
|
2369
|
-
class EdsIconComponent {
|
|
2370
|
-
constructor() {
|
|
2371
|
-
this.name = input.required();
|
|
2372
|
-
this.size = input(20);
|
|
2373
|
-
this.fill = input(0);
|
|
2374
|
-
this.weight = input(400);
|
|
2375
|
-
this.variation = computed(() => `'FILL' ${this.fill()}, 'wght' ${this.weight()}, 'GRAD' 0, 'opsz' ${this.size()}`);
|
|
2376
|
-
}
|
|
2377
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2378
|
-
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 }); }
|
|
2379
|
-
}
|
|
2380
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, decorators: [{
|
|
2381
|
-
type: Component,
|
|
2382
|
-
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"] }]
|
|
2383
|
-
}] });
|
|
2384
|
-
|
|
2385
|
-
class EdsPulseDotComponent {
|
|
2386
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2387
|
-
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(--eds-green-500)}.eds-pulse-dot__core:after{content:\"\";position:absolute;inset:0;border-radius:50%;background:var(--eds-green-500);animation:eds-ping 1.4s cubic-bezier(0,0,.2,1) infinite}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2388
|
-
}
|
|
2389
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, decorators: [{
|
|
2390
|
-
type: Component,
|
|
2391
|
-
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(--eds-green-500)}.eds-pulse-dot__core:after{content:\"\";position:absolute;inset:0;border-radius:50%;background:var(--eds-green-500);animation:eds-ping 1.4s cubic-bezier(0,0,.2,1) infinite}\n"] }]
|
|
2392
|
-
}] });
|
|
2393
|
-
|
|
2394
|
-
class EdsModuleNavComponent {
|
|
2395
|
-
constructor() {
|
|
2396
|
-
this.config = inject(VOICE_RECEPTIONIST_CONFIG);
|
|
2397
|
-
this.session = inject(VOICE_RECEPTIONIST_SESSION);
|
|
2398
|
-
this.agentOnline = inject(AgentOnlineService);
|
|
2399
|
-
this.agentOn = this.agentOnline.online;
|
|
2400
|
-
this.includePracticeNav = !!this.config.includePracticeNav;
|
|
2401
|
-
this.receptionistNav = [
|
|
2402
|
-
{ id: 'dashboard', label: 'Dashboard', icon: 'space_dashboard', route: 'dashboard' },
|
|
2403
|
-
{ id: 'live', label: 'Live Calls', icon: 'graphic_eq', route: 'live', pulse: true },
|
|
2404
|
-
{ id: 'history', label: 'Call History', icon: 'history', route: 'history' },
|
|
2405
|
-
{ id: 'analytics', label: 'Analytics', icon: 'monitoring', route: 'analytics' },
|
|
2406
|
-
{ id: 'knowledge', label: 'Knowledge Base', icon: 'menu_book', route: 'knowledge' },
|
|
2407
|
-
{ id: 'settings', label: 'Agent Settings', icon: 'tune', route: 'settings' },
|
|
2408
|
-
{ id: 'audit', label: 'Audit Log', icon: 'verified', route: 'audit' },
|
|
2409
|
-
];
|
|
2410
|
-
this.practiceNav = [
|
|
2411
|
-
{ id: 'patients', label: 'Patients', icon: 'groups', route: 'patients' },
|
|
2412
|
-
{ id: 'providers', label: 'Providers', icon: 'medical_services', route: 'providers' },
|
|
2413
|
-
{ id: 'appointments', label: 'Appointments', icon: 'calendar_month', route: 'appointments' },
|
|
2414
|
-
{ id: 'admin', label: 'Admin', icon: 'admin_panel_settings', route: 'admin', adminOnly: true },
|
|
2415
|
-
];
|
|
2416
|
-
if (this.agentOnline.online() === null && !this.agentOnline.loading()) {
|
|
2417
|
-
this.agentOnline.refresh();
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
showNavItem(item) {
|
|
2421
|
-
return !item.adminOnly || !!this.session?.isAdmin();
|
|
2422
|
-
}
|
|
2423
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2424
|
-
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(--eds-surface-l0);border-right:1px solid var(--eds-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(--eds-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(--eds-font-inter);font-size:14px;font-weight:600;color:var(--eds-text-primary)}.eds-modnav-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--eds-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(--eds-text-secondary);font-size:13px;font-weight:500;position:relative}.eds-modnav-links a:hover{background:var(--eds-surface-l1);color:var(--eds-text-primary)}.eds-modnav-links a.active{background:#3681f41a;color:var(--eds-brand-600)}.eds-modnav-links--practice{margin-bottom:12px}.eds-modnav-section{margin:14px 10px 6px;font-family:var(--eds-font-inter);font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--eds-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(--eds-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(--eds-border-default);background:var(--eds-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(--eds-gray-200);border-radius:999px;overflow:hidden}.eds-readiness-bar span{display:block;height:100%;background:linear-gradient(90deg,var(--eds-brand-500),var(--eds-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 }); }
|
|
2425
|
-
}
|
|
2426
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, decorators: [{
|
|
2427
|
-
type: Component,
|
|
2428
|
-
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(--eds-surface-l0);border-right:1px solid var(--eds-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(--eds-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(--eds-font-inter);font-size:14px;font-weight:600;color:var(--eds-text-primary)}.eds-modnav-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--eds-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(--eds-text-secondary);font-size:13px;font-weight:500;position:relative}.eds-modnav-links a:hover{background:var(--eds-surface-l1);color:var(--eds-text-primary)}.eds-modnav-links a.active{background:#3681f41a;color:var(--eds-brand-600)}.eds-modnav-links--practice{margin-bottom:12px}.eds-modnav-section{margin:14px 10px 6px;font-family:var(--eds-font-inter);font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--eds-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(--eds-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(--eds-border-default);background:var(--eds-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(--eds-gray-200);border-radius:999px;overflow:hidden}.eds-readiness-bar span{display:block;height:100%;background:linear-gradient(90deg,var(--eds-brand-500),var(--eds-violet-500));border-radius:999px}\n"] }]
|
|
2429
|
-
}], ctorParameters: () => [] });
|
|
2430
|
-
|
|
2431
|
-
/**
|
|
2432
|
-
* Lightweight, reusable modal shell.
|
|
2433
|
-
*
|
|
2434
|
-
* Renders a centered card on top of a dimmed backdrop. Closes on backdrop
|
|
2435
|
-
* click, ESC key, or via the close button. Slot the modal body using
|
|
2436
|
-
* <ng-content>.
|
|
2437
|
-
*/
|
|
2438
|
-
class ModalComponent {
|
|
2439
|
-
constructor() {
|
|
2440
|
-
this.maxWidth = '640px';
|
|
2441
|
-
this.closed = new EventEmitter();
|
|
2442
|
-
}
|
|
2443
|
-
onEscape() {
|
|
2444
|
-
this.closed.emit();
|
|
2445
|
-
}
|
|
2446
|
-
onBackdropClick(event) {
|
|
2447
|
-
if (event.target === event.currentTarget) {
|
|
2448
|
-
this.closed.emit();
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
close() {
|
|
2452
|
-
this.closed.emit();
|
|
2453
|
-
}
|
|
2454
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2455
|
-
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(--eds-color-surface);border-radius:var(--eds-radius-lg);border:1px solid var(--eds-color-border);box-shadow:var(--eds-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(--eds-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(--eds-color-text)}.modal-subtitle{margin:0;font-size:13px;color:var(--eds-color-text-muted)}.modal-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--eds-radius-md);border:1px solid transparent;background:transparent;color:var(--eds-color-text-muted);transition:background .12s ease,color .12s ease}.modal-close:hover{background:var(--eds-color-surface-alt);color:var(--eds-color-text)}.modal-close:focus-visible{outline:2px solid var(--eds-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 }); }
|
|
2456
|
-
}
|
|
2457
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, decorators: [{
|
|
2458
|
-
type: Component,
|
|
2459
|
-
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(--eds-color-surface);border-radius:var(--eds-radius-lg);border:1px solid var(--eds-color-border);box-shadow:var(--eds-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(--eds-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(--eds-color-text)}.modal-subtitle{margin:0;font-size:13px;color:var(--eds-color-text-muted)}.modal-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--eds-radius-md);border:1px solid transparent;background:transparent;color:var(--eds-color-text-muted);transition:background .12s ease,color .12s ease}.modal-close:hover{background:var(--eds-color-surface-alt);color:var(--eds-color-text)}.modal-close:focus-visible{outline:2px solid var(--eds-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"] }]
|
|
2460
|
-
}], propDecorators: { title: [{
|
|
2461
|
-
type: Input,
|
|
2462
|
-
args: [{ required: true }]
|
|
2463
|
-
}], subtitle: [{
|
|
2464
|
-
type: Input
|
|
2465
|
-
}], maxWidth: [{
|
|
2466
|
-
type: Input
|
|
2467
|
-
}], closed: [{
|
|
2468
|
-
type: Output
|
|
2469
|
-
}], onEscape: [{
|
|
2470
|
-
type: HostListener,
|
|
2471
|
-
args: ['document:keydown.escape']
|
|
2472
|
-
}] } });
|
|
2473
|
-
|
|
2474
|
-
class EdsButtonComponent {
|
|
2475
|
-
constructor() {
|
|
2476
|
-
this.variant = input('secondary');
|
|
2477
|
-
this.type = input('button');
|
|
2478
|
-
this.disabled = input(false);
|
|
2479
|
-
}
|
|
2480
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2481
|
-
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: `
|
|
2482
|
-
<button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
|
|
2483
|
-
<ng-content />
|
|
2484
|
-
</button>
|
|
2485
|
-
`, 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(--eds-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(--eds-brand-600);color:#fff}.eds-btn[data-variant=primary]:hover:not(:disabled){background:var(--eds-brand-500)}.eds-btn[data-variant=secondary]{background:var(--eds-surface-l0);color:var(--eds-text-primary);border-color:var(--eds-border-default)}.eds-btn[data-variant=secondary]:hover:not(:disabled){background:var(--eds-surface-l1)}.eds-btn[data-variant=ghost]{background:transparent;color:var(--eds-text-secondary)}.eds-btn[data-variant=ghost]:hover:not(:disabled){background:var(--eds-gray-100);color:var(--eds-text-primary)}.eds-btn[data-variant=danger]{background:#f037401a;color:var(--eds-red-500);border-color:#f0374040}.eds-btn[data-variant=positive]{background:#0abf521f;color:var(--eds-green-500);border-color:#0abf5240}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2486
|
-
}
|
|
2487
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, decorators: [{
|
|
2488
|
-
type: Component,
|
|
2489
|
-
args: [{ selector: 'eds-button', standalone: true, template: `
|
|
2490
|
-
<button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
|
|
2491
|
-
<ng-content />
|
|
2492
|
-
</button>
|
|
2493
|
-
`, 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(--eds-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(--eds-brand-600);color:#fff}.eds-btn[data-variant=primary]:hover:not(:disabled){background:var(--eds-brand-500)}.eds-btn[data-variant=secondary]{background:var(--eds-surface-l0);color:var(--eds-text-primary);border-color:var(--eds-border-default)}.eds-btn[data-variant=secondary]:hover:not(:disabled){background:var(--eds-surface-l1)}.eds-btn[data-variant=ghost]{background:transparent;color:var(--eds-text-secondary)}.eds-btn[data-variant=ghost]:hover:not(:disabled){background:var(--eds-gray-100);color:var(--eds-text-primary)}.eds-btn[data-variant=danger]{background:#f037401a;color:var(--eds-red-500);border-color:#f0374040}.eds-btn[data-variant=positive]{background:#0abf521f;color:var(--eds-green-500);border-color:#0abf5240}\n"] }]
|
|
2494
|
-
}] });
|
|
2495
|
-
|
|
2496
|
-
const PAGE_TITLES = {
|
|
2497
|
-
dashboard: 'Dashboard',
|
|
2498
|
-
live: 'Live Calls',
|
|
2499
|
-
history: 'Call History',
|
|
2500
|
-
tasks: 'Tasks & Follow-ups',
|
|
2501
|
-
analytics: 'Analytics',
|
|
2502
|
-
settings: 'Agent Settings',
|
|
2503
|
-
knowledge: 'Knowledge Base',
|
|
2504
|
-
audit: 'Audit Log',
|
|
2505
|
-
wizard: 'Setup Wizard',
|
|
2506
|
-
patients: 'Patients',
|
|
2507
|
-
providers: 'Providers',
|
|
2508
|
-
appointments: 'Appointments',
|
|
2509
|
-
admin: 'Admin',
|
|
2510
|
-
};
|
|
2511
|
-
class EdsTopbarComponent {
|
|
2512
|
-
constructor() {
|
|
2513
|
-
this.router = inject(Router);
|
|
2514
|
-
this.sessionAdapter = inject(VOICE_RECEPTIONIST_SESSION);
|
|
2515
|
-
this.toast = inject(ToastService);
|
|
2516
|
-
this.agentOnline = inject(AgentOnlineService);
|
|
2517
|
-
this.pageTitle = signal('Dashboard');
|
|
2518
|
-
this.confirmOffOpen = signal(false);
|
|
2519
|
-
this.online = this.agentOnline.online;
|
|
2520
|
-
this.loading = this.agentOnline.loading;
|
|
2521
|
-
this.toggling = this.agentOnline.toggling;
|
|
2522
|
-
this.statusError = this.agentOnline.error;
|
|
2523
|
-
this.userEmail = computed(() => this.sessionAdapter?.email() ?? null);
|
|
2524
|
-
this.showUser = computed(() => !!this.userEmail());
|
|
2525
|
-
this.showLogout = computed(() => !!this.sessionAdapter);
|
|
2526
|
-
this.userInitials = computed(() => {
|
|
2527
|
-
const email = this.userEmail()?.trim() ?? '';
|
|
2528
|
-
if (!email)
|
|
2529
|
-
return '?';
|
|
2530
|
-
const local = email.split('@')[0] ?? '';
|
|
2531
|
-
const parts = local.split(/[._-]+/).filter(Boolean);
|
|
2532
|
-
if (parts.length >= 2) {
|
|
2533
|
-
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
2534
|
-
}
|
|
2535
|
-
return local.slice(0, 2).toUpperCase() || '?';
|
|
2536
|
-
});
|
|
2537
|
-
this.statusLabel = computed(() => {
|
|
2538
|
-
if (this.loading() && this.online() === null)
|
|
2539
|
-
return 'Checking status…';
|
|
2540
|
-
if (this.online() === null)
|
|
2541
|
-
return 'Status unknown';
|
|
2542
|
-
return this.online()
|
|
2543
|
-
? 'AI receptionist is answering calls'
|
|
2544
|
-
: 'AI is offline — calls go to front desk';
|
|
2545
|
-
});
|
|
2546
|
-
this.switchDisabled = computed(() => this.loading() || this.toggling() || this.online() === null);
|
|
2547
|
-
this.agentOnline.refresh();
|
|
2548
|
-
this.updateTitle(this.router.url);
|
|
2549
|
-
this.router.events
|
|
2550
|
-
.pipe(filter((e) => e instanceof NavigationEnd))
|
|
2551
|
-
.subscribe((e) => this.updateTitle(e.urlAfterRedirects));
|
|
2552
|
-
}
|
|
2553
|
-
onToggleClick() {
|
|
2554
|
-
if (this.switchDisabled())
|
|
2555
|
-
return;
|
|
2556
|
-
if (this.online()) {
|
|
2557
|
-
this.confirmOffOpen.set(true);
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
|
-
this.applyOnline(true);
|
|
2561
|
-
}
|
|
2562
|
-
cancelConfirmOff() {
|
|
2563
|
-
this.confirmOffOpen.set(false);
|
|
2564
|
-
}
|
|
2565
|
-
confirmTurnOff() {
|
|
2566
|
-
this.confirmOffOpen.set(false);
|
|
2567
|
-
this.applyOnline(false);
|
|
2568
|
-
}
|
|
2569
|
-
applyOnline(enabled) {
|
|
2570
|
-
this.agentOnline.setOnline(enabled).subscribe({
|
|
2571
|
-
next: () => {
|
|
2572
|
-
this.toast.show(enabled
|
|
2573
|
-
? 'AI receptionist is online — Ava will answer inbound calls.'
|
|
2574
|
-
: 'AI receptionist is offline — calls go to the front desk.', 'success');
|
|
2575
|
-
},
|
|
2576
|
-
error: (err) => {
|
|
2577
|
-
this.toast.show(err.message ?? 'Failed to update agent status.', 'error');
|
|
2578
|
-
this.agentOnline.refresh();
|
|
2579
|
-
},
|
|
2580
|
-
});
|
|
2581
|
-
}
|
|
2582
|
-
logout() {
|
|
2583
|
-
this.sessionAdapter?.logout();
|
|
2584
|
-
}
|
|
2585
|
-
updateTitle(url) {
|
|
2586
|
-
const base = url.split('?')[0].split(';')[0];
|
|
2587
|
-
const segments = base.split('/').filter(Boolean);
|
|
2588
|
-
const last = segments[segments.length - 1] ?? '';
|
|
2589
|
-
const prev = segments[segments.length - 2] ?? '';
|
|
2590
|
-
if (prev === 'live' && last && last !== 'live') {
|
|
2591
|
-
this.pageTitle.set('Live Monitor');
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
if (prev === 'history' && last && last !== 'history') {
|
|
2595
|
-
this.pageTitle.set('Call Detail');
|
|
2596
|
-
return;
|
|
2597
|
-
}
|
|
2598
|
-
if (prev === 'settings' && last === 'outbound-purposes') {
|
|
2599
|
-
this.pageTitle.set('Outbound Purposes');
|
|
2600
|
-
return;
|
|
2601
|
-
}
|
|
2602
|
-
this.pageTitle.set(PAGE_TITLES[last] ?? 'AI Receptionist');
|
|
2603
|
-
}
|
|
2604
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2605
|
-
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(--eds-surface-l0);border-bottom:1px solid var(--eds-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(--eds-text-secondary)}.eds-breadcrumb-current{color:var(--eds-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(--eds-border-default);background:var(--eds-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(--eds-surface-l1)}.eds-agent-toggle__track{width:36px;height:20px;border-radius:999px;background:var(--eds-gray-300);position:relative;transition:background .15s ease;flex-shrink:0}.eds-agent-toggle__track.on{background:var(--eds-green-500)}.eds-agent-toggle__track.unknown{background:var(--eds-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(--eds-text-primary);white-space:nowrap}.eds-agent-toggle__hint{font-size:11px;line-height:1.3;color:var(--eds-text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.eds-agent-retry{border:none;background:transparent;color:var(--eds-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(--eds-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(--eds-gray-200);color:var(--eds-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(--eds-text-secondary)}.eds-topbar-logout{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--eds-border-default);background:var(--eds-surface-l0);color:var(--eds-text-secondary);border-radius:var(--eds-radius-md);padding:6px 10px;cursor:pointer;font:inherit}.eds-topbar-logout:hover{background:var(--eds-surface-l1);color:var(--eds-text-primary);border-color:var(--eds-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 }); }
|
|
2606
|
-
}
|
|
2607
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, decorators: [{
|
|
2608
|
-
type: Component,
|
|
2609
|
-
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(--eds-surface-l0);border-bottom:1px solid var(--eds-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(--eds-text-secondary)}.eds-breadcrumb-current{color:var(--eds-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(--eds-border-default);background:var(--eds-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(--eds-surface-l1)}.eds-agent-toggle__track{width:36px;height:20px;border-radius:999px;background:var(--eds-gray-300);position:relative;transition:background .15s ease;flex-shrink:0}.eds-agent-toggle__track.on{background:var(--eds-green-500)}.eds-agent-toggle__track.unknown{background:var(--eds-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(--eds-text-primary);white-space:nowrap}.eds-agent-toggle__hint{font-size:11px;line-height:1.3;color:var(--eds-text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.eds-agent-retry{border:none;background:transparent;color:var(--eds-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(--eds-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(--eds-gray-200);color:var(--eds-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(--eds-text-secondary)}.eds-topbar-logout{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--eds-border-default);background:var(--eds-surface-l0);color:var(--eds-text-secondary);border-radius:var(--eds-radius-md);padding:6px 10px;cursor:pointer;font:inherit}.eds-topbar-logout:hover{background:var(--eds-surface-l1);color:var(--eds-text-primary);border-color:var(--eds-gray-300)}@media(max-width:1100px){.eds-topbar-user__email{display:none}}\n"] }]
|
|
2610
|
-
}], ctorParameters: () => [] });
|
|
2611
|
-
|
|
2612
|
-
class EdsAppShellComponent {
|
|
2613
|
-
constructor() {
|
|
2614
|
-
/** EHR embed: false (default). Standalone Voice Hub: set showModuleNav: true. */
|
|
2615
|
-
this.showModuleNav = !!inject(VOICE_RECEPTIONIST_CONFIG).showModuleNav;
|
|
2616
|
-
}
|
|
2617
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2618
|
-
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 <app-toast-container />\n</div>\n", styles: [":host{display:block}.eds-layout{display:flex;min-height:100vh;background:var(--eds-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 }); }
|
|
2619
|
-
}
|
|
2620
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, decorators: [{
|
|
2621
|
-
type: Component,
|
|
2622
|
-
args: [{ selector: 'eds-app-shell', imports: [
|
|
2623
|
-
RouterOutlet,
|
|
2624
|
-
EdsModuleNavComponent,
|
|
2625
|
-
EdsTopbarComponent,
|
|
2626
|
-
ToastContainerComponent,
|
|
2627
|
-
], 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 <app-toast-container />\n</div>\n", styles: [":host{display:block}.eds-layout{display:flex;min-height:100vh;background:var(--eds-surface-l1)}.eds-main{flex:1;min-width:0;display:flex;flex-direction:column}.eds-content{flex:1;overflow:auto}\n"] }]
|
|
2628
|
-
}] });
|
|
2629
|
-
|
|
2630
|
-
/** Feature child routes (under the embed shell). */
|
|
2631
|
-
const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
2632
|
-
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
2633
|
-
{
|
|
2634
|
-
path: 'dashboard',
|
|
2635
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-BNdnbAVM.mjs').then((m) => m.DashboardPageComponent),
|
|
2636
|
-
},
|
|
2637
|
-
{
|
|
2638
|
-
path: 'live',
|
|
2639
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-DfLmK89M.mjs').then((m) => m.LiveCallsPageComponent),
|
|
2640
|
-
},
|
|
2641
|
-
{
|
|
2642
|
-
path: 'live/:id',
|
|
2643
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-DNpLFqBw.mjs').then((m) => m.LiveMonitorPageComponent),
|
|
2644
|
-
},
|
|
2645
|
-
{
|
|
2646
|
-
path: 'history',
|
|
2647
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-B5AnEjoh.mjs').then((m) => m.CallHistoryPageComponent),
|
|
2648
|
-
},
|
|
2649
|
-
{
|
|
2650
|
-
path: 'history/:id',
|
|
2651
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-2K9W77As.mjs').then((m) => m.CallHistoryDetailPageComponent),
|
|
2652
|
-
},
|
|
2653
|
-
{
|
|
2654
|
-
path: 'tasks',
|
|
2655
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-DXxjMVUv.mjs').then((m) => m.TasksPageComponent),
|
|
2656
|
-
},
|
|
2657
|
-
{
|
|
2658
|
-
path: 'analytics',
|
|
2659
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-Dsujn5az.mjs').then((m) => m.AnalyticsPageComponent),
|
|
2660
|
-
},
|
|
2661
|
-
{
|
|
2662
|
-
path: 'settings',
|
|
2663
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-settings-page.component-B9nv6wtB.mjs').then((m) => m.SettingsPageComponent),
|
|
2664
|
-
},
|
|
2665
|
-
{
|
|
2666
|
-
path: 'settings/outbound-purposes',
|
|
2667
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-CYSZ832p.mjs').then((m) => m.OutboundPurposesPageComponent),
|
|
2668
|
-
},
|
|
2669
|
-
{
|
|
2670
|
-
path: 'knowledge',
|
|
2671
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs').then((m) => m.KnowledgeHubPageComponent),
|
|
2672
|
-
},
|
|
2673
|
-
{
|
|
2674
|
-
path: 'audit',
|
|
2675
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-xoLlHj8K.mjs').then((m) => m.AuditPageComponent),
|
|
2676
|
-
},
|
|
2677
|
-
{
|
|
2678
|
-
path: 'wizard',
|
|
2679
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-C1Z6yC0q.mjs').then((m) => m.WizardPageComponent),
|
|
2680
|
-
},
|
|
2681
|
-
];
|
|
2682
|
-
/**
|
|
2683
|
-
* Lazy-loadable routes for EHR (and standalone hosts).
|
|
2684
|
-
* Mount under a host path, e.g. `{ path: 'ai-receptionist', children: VOICE_RECEPTIONIST_ROUTES }`.
|
|
2685
|
-
*/
|
|
2686
|
-
const VOICE_RECEPTIONIST_ROUTES = [
|
|
2687
|
-
{
|
|
2688
|
-
path: '',
|
|
2689
|
-
component: EdsAppShellComponent,
|
|
2690
|
-
children: VOICE_RECEPTIONIST_FEATURE_ROUTES,
|
|
2691
|
-
},
|
|
2692
|
-
];
|
|
2693
|
-
|
|
2694
|
-
class EdsBadgeComponent {
|
|
2695
|
-
constructor() {
|
|
2696
|
-
this.tone = input('gray');
|
|
2697
|
-
}
|
|
2698
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2699
|
-
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(--eds-font-inter);font-size:11px;font-weight:600;line-height:1.4;white-space:nowrap}.eds-badge[data-tone=brand]{background:#3681f41f;color:var(--eds-brand-600)}.eds-badge[data-tone=pos]{background:#0abf521f;color:var(--eds-green-500)}.eds-badge[data-tone=notice]{background:#ffb90026;color:#b45309}.eds-badge[data-tone=neg]{background:#f037401f;color:var(--eds-red-500)}.eds-badge[data-tone=violet]{background:#8e51ff1f;color:var(--eds-violet-500)}.eds-badge[data-tone=gray]{background:var(--eds-gray-100);color:var(--eds-gray-600)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2700
|
-
}
|
|
2701
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, decorators: [{
|
|
2702
|
-
type: Component,
|
|
2703
|
-
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(--eds-font-inter);font-size:11px;font-weight:600;line-height:1.4;white-space:nowrap}.eds-badge[data-tone=brand]{background:#3681f41f;color:var(--eds-brand-600)}.eds-badge[data-tone=pos]{background:#0abf521f;color:var(--eds-green-500)}.eds-badge[data-tone=notice]{background:#ffb90026;color:#b45309}.eds-badge[data-tone=neg]{background:#f037401f;color:var(--eds-red-500)}.eds-badge[data-tone=violet]{background:#8e51ff1f;color:var(--eds-violet-500)}.eds-badge[data-tone=gray]{background:var(--eds-gray-100);color:var(--eds-gray-600)}\n"] }]
|
|
2704
|
-
}] });
|
|
2705
|
-
|
|
2706
|
-
/*
|
|
2707
|
-
* Public API Surface of @edvak99/voice-receptionist-widget
|
|
2708
|
-
*/
|
|
2709
|
-
|
|
2710
|
-
/**
|
|
2711
|
-
* Generated bundle index. Do not edit.
|
|
2712
|
-
*/
|
|
2713
|
-
|
|
2714
|
-
export { AiReceptionistApiService as A, DEFAULT_PAGE_SIZE 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, totalPages as f, buildAutoWelcomeTemplate as g, resolveWelcomePreview as h, DEFAULT_PLACEHOLDERS as i, asPaginatedResult as j, EdsAppShellComponent as k, VOICE_RECEPTIONIST_FEATURE_ROUTES as l, VOICE_RECEPTIONIST_ROUTES as m, VOICE_RECEPTIONIST_SESSION as n, provideVoiceReceptionist as p, resolveToolKey as r, toolEventTrackKey as t };
|
|
2715
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.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
|