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