@edvak99/voice-receptionist-widget 1.0.0 → 1.1.0
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 +56 -43
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs +113 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs +35 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs +316 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs +138 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs +110 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs +29 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +70 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs +248 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +27 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs +136 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs +2666 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs +575 -0
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs +120 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs +115 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs +574 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +54 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs +265 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs +509 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +52 -0
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs +237 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs +45 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -41
- package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
- package/lib/api-paths.d.ts +11 -0
- package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +45 -0
- package/lib/components/call-recording-player/call-recording-player.component.d.ts +29 -0
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +19 -0
- package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +36 -0
- package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +37 -0
- package/lib/components/outbound-purpose-form-modal/outbound-purpose-form-modal.component.d.ts +38 -0
- package/lib/components/task-detail-drawer/task-detail-drawer.component.d.ts +20 -0
- package/lib/config.d.ts +23 -0
- package/lib/extract-api-rows.d.ts +2 -0
- package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +22 -0
- package/lib/knowledge/components/faq-form/faq-form.component.d.ts +27 -0
- package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +17 -0
- package/lib/knowledge/mocks/practice-faq.seed.d.ts +2 -0
- package/lib/knowledge/models/practice-faq.model.d.ts +47 -0
- package/lib/knowledge/pages/knowledge-hub.page.d.ts +47 -0
- package/lib/knowledge/services/practice-faq-api.mock.d.ts +16 -0
- package/lib/knowledge/services/practice-faq-api.service.d.ts +22 -0
- package/lib/knowledge/utils/faq.util.d.ts +3 -0
- package/lib/layout/eds-app-rail.component.d.ts +6 -0
- package/lib/layout/eds-app-shell.component.d.ts +5 -0
- package/lib/layout/eds-module-nav.component.d.ts +24 -0
- package/lib/layout/eds-topbar.component.d.ts +29 -0
- package/lib/mocks/api-log.seed.d.ts +5 -0
- package/lib/mocks/outbound-purpose.seed.d.ts +3 -0
- package/lib/mocks/seed-data.d.ts +38 -0
- package/lib/models/ai-receptionist.types.d.ts +257 -0
- package/lib/models/api-dto.types.d.ts +215 -0
- package/lib/models/api-log.types.d.ts +53 -0
- package/lib/models/live-transcript.types.d.ts +26 -0
- package/lib/models/outbound-purpose.types.d.ts +50 -0
- package/lib/pages/analytics-page/analytics-page.component.d.ts +44 -0
- package/lib/pages/audit-page/audit-page.component.d.ts +14 -0
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +25 -0
- package/lib/pages/call-history-page/call-history-page.component.d.ts +42 -0
- package/lib/pages/dashboard-page/dashboard-page.component.d.ts +60 -0
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +37 -0
- package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +43 -0
- package/lib/pages/outbound-purposes-page/outbound-purposes-page.component.d.ts +24 -0
- package/lib/pages/settings-page/settings-fields.config.d.ts +35 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +50 -0
- package/lib/pages/settings-page/tts-voice-presets.d.ts +7 -0
- package/lib/pages/tasks-page/tasks-page.component.d.ts +35 -0
- package/lib/pages/wizard-page/wizard-page.component.d.ts +17 -0
- package/lib/pipes/eds-elapsed.pipe.d.ts +7 -0
- package/lib/provide-voice-receptionist.d.ts +6 -0
- package/lib/routes.d.ts +8 -0
- package/lib/services/agent-online.service.d.ts +29 -0
- package/lib/services/ai-receptionist-api.mappers.d.ts +45 -0
- package/lib/services/ai-receptionist-api.mock.d.ts +150 -0
- package/lib/services/ai-receptionist-api.service.d.ts +156 -0
- package/lib/services/call-review.service.d.ts +12 -0
- package/lib/services/dashboard-socket.service.d.ts +26 -0
- package/lib/services/live-calls-list.service.d.ts +29 -0
- package/lib/services/live-transcript.service.d.ts +39 -0
- package/lib/shared/copy-button.component.d.ts +14 -0
- package/lib/shared/modal.component.d.ts +20 -0
- package/lib/shared/toast-container.component.d.ts +7 -0
- package/lib/shared/toast.service.d.ts +16 -0
- package/lib/ui/eds-avatar.component.d.ts +8 -0
- package/lib/ui/eds-badge.component.d.ts +7 -0
- package/lib/ui/eds-button.component.d.ts +9 -0
- package/lib/ui/eds-icon.component.d.ts +10 -0
- package/lib/ui/eds-pulse-dot.component.d.ts +5 -0
- package/lib/ui/eds-waveform.component.d.ts +8 -0
- package/lib/utils/call-outcome.util.d.ts +8 -0
- package/lib/utils/call-review.util.d.ts +8 -0
- package/lib/utils/outbound-purpose.util.d.ts +14 -0
- package/lib/utils/task.util.d.ts +10 -0
- package/lib/utils/tool-labels.util.d.ts +14 -0
- package/lib/utils/transcript-timing.util.d.ts +17 -0
- package/lib/utils/welcome-message.util.d.ts +5 -0
- package/package.json +12 -5
- package/public-api.d.ts +6 -2
- package/styles/_index.scss +55 -0
- package/styles/chat-ui.scss +133 -0
- package/styles/eds.scss +328 -0
- package/lib/voice-receptionist-widget.component.d.ts +0 -5
- package/lib/voice-receptionist-widget.service.d.ts +0 -6
package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs
ADDED
|
@@ -0,0 +1,2666 @@
|
|
|
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
|
+
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
6
|
+
import { filter } from 'rxjs/operators';
|
|
7
|
+
|
|
8
|
+
const VOICE_RECEPTIONIST_CONFIG = new InjectionToken('VOICE_RECEPTIONIST_CONFIG');
|
|
9
|
+
const VOICE_RECEPTIONIST_SESSION = new InjectionToken('VOICE_RECEPTIONIST_SESSION', { factory: () => null });
|
|
10
|
+
|
|
11
|
+
function provideVoiceReceptionist(options) {
|
|
12
|
+
const { session = null, ...config } = options;
|
|
13
|
+
return makeEnvironmentProviders([
|
|
14
|
+
{ provide: VOICE_RECEPTIONIST_CONFIG, useValue: config },
|
|
15
|
+
{ provide: VOICE_RECEPTIONIST_SESSION, useValue: session },
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ToastService {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.nextId = 1;
|
|
22
|
+
this.timers = new Map();
|
|
23
|
+
this._toasts = signal([]);
|
|
24
|
+
this.toasts = this._toasts.asReadonly();
|
|
25
|
+
}
|
|
26
|
+
show(text, variant = 'info', durationMs = 3500) {
|
|
27
|
+
const id = this.nextId++;
|
|
28
|
+
this._toasts.update((list) => [...list, { id, text, variant }]);
|
|
29
|
+
const timer = setTimeout(() => this.dismiss(id), durationMs);
|
|
30
|
+
this.timers.set(id, timer);
|
|
31
|
+
}
|
|
32
|
+
dismiss(id) {
|
|
33
|
+
const timer = this.timers.get(id);
|
|
34
|
+
if (timer) {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
this.timers.delete(id);
|
|
37
|
+
}
|
|
38
|
+
this._toasts.update((list) => list.filter((t) => t.id !== id));
|
|
39
|
+
}
|
|
40
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
41
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, providedIn: 'root' }); }
|
|
42
|
+
}
|
|
43
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastService, decorators: [{
|
|
44
|
+
type: Injectable,
|
|
45
|
+
args: [{ providedIn: 'root' }]
|
|
46
|
+
}] });
|
|
47
|
+
|
|
48
|
+
class ToastContainerComponent {
|
|
49
|
+
constructor() {
|
|
50
|
+
this.toastService = inject(ToastService);
|
|
51
|
+
}
|
|
52
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
53
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: ToastContainerComponent, isStandalone: true, selector: "app-toast-container", ngImport: i0, template: "<div class=\"toast-stack\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div class=\"toast\" [class]=\"'toast-' + toast.variant\" role=\"status\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n @switch (toast.variant) {\n @case ('success') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n @case ('error') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"13\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n @default {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n }\n </span>\n <span class=\"toast-text\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"toast-dismiss\"\n (click)=\"toastService.dismiss(toast.id)\"\n aria-label=\"Dismiss notification\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;bottom:24px;right:24px;z-index:1100;pointer-events:none}.toast-stack{display:flex;flex-direction:column;gap:10px;max-width:380px}.toast{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--color-text);animation:slide-in .16s ease}.toast-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;flex-shrink:0}.toast-info .toast-icon{background:var(--color-primary-soft);color:var(--color-primary)}.toast-success .toast-icon{background:var(--color-success-soft);color:var(--color-success)}.toast-error .toast-icon{background:var(--color-danger-soft);color:var(--color-danger)}.toast-text{flex:1;line-height:1.45}.toast-dismiss{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--color-surface-alt);color:var(--color-text)}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
54
|
+
}
|
|
55
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
56
|
+
type: Component,
|
|
57
|
+
args: [{ selector: 'app-toast-container', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toast-stack\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div class=\"toast\" [class]=\"'toast-' + toast.variant\" role=\"status\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n @switch (toast.variant) {\n @case ('success') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n @case ('error') {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"13\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n @default {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\" />\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\" />\n </svg>\n }\n }\n </span>\n <span class=\"toast-text\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"toast-dismiss\"\n (click)=\"toastService.dismiss(toast.id)\"\n aria-label=\"Dismiss notification\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;bottom:24px;right:24px;z-index:1100;pointer-events:none}.toast-stack{display:flex;flex-direction:column;gap:10px;max-width:380px}.toast{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 6px 24px #141e321f;font-size:13px;color:var(--color-text);animation:slide-in .16s ease}.toast-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;flex-shrink:0}.toast-info .toast-icon{background:var(--color-primary-soft);color:var(--color-primary)}.toast-success .toast-icon{background:var(--color-success-soft);color:var(--color-success)}.toast-error .toast-icon{background:var(--color-danger-soft);color:var(--color-danger)}.toast-text{flex:1;line-height:1.45}.toast-dismiss{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;color:var(--color-text-soft);border-radius:4px;transition:background .12s ease,color .12s ease}.toast-dismiss:hover{background:var(--color-surface-alt);color:var(--color-text)}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
58
|
+
}] });
|
|
59
|
+
|
|
60
|
+
class EdsIconComponent {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.name = input.required();
|
|
63
|
+
this.size = input(20);
|
|
64
|
+
this.fill = input(0);
|
|
65
|
+
this.weight = input(400);
|
|
66
|
+
this.variation = computed(() => `'FILL' ${this.fill()}, 'wght' ${this.weight()}, 'GRAD' 0, 'opsz' ${this.size()}`);
|
|
67
|
+
}
|
|
68
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
69
|
+
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 }); }
|
|
70
|
+
}
|
|
71
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsIconComponent, decorators: [{
|
|
72
|
+
type: Component,
|
|
73
|
+
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"] }]
|
|
74
|
+
}] });
|
|
75
|
+
|
|
76
|
+
class EdsAppRailComponent {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.includePracticeNav = !!inject(VOICE_RECEPTIONIST_CONFIG).includePracticeNav;
|
|
79
|
+
}
|
|
80
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
81
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsAppRailComponent, isStandalone: true, selector: "eds-app-rail", ngImport: i0, template: "<aside class=\"eds-rail\" aria-label=\"EDS product navigation\">\n <a routerLink=\"dashboard\" class=\"eds-rail-logo\" title=\"Edvak\">e</a>\n <nav class=\"eds-rail-nav\">\n <a routerLink=\"dashboard\" routerLinkActive=\"active\" title=\"Dashboard\">\n <eds-icon name=\"space_dashboard\" [size]=\"22\" />\n </a>\n @if (includePracticeNav) {\n <a routerLink=\"appointments\" routerLinkActive=\"active\" title=\"Appointments\">\n <eds-icon name=\"calendar_month\" [size]=\"22\" />\n </a>\n <a routerLink=\"patients\" routerLinkActive=\"active\" title=\"Patients\">\n <eds-icon name=\"groups\" [size]=\"22\" />\n </a>\n }\n <span title=\"Chat (coming soon)\"><eds-icon name=\"chat\" [size]=\"22\" /></span>\n <a routerLink=\"live\" routerLinkActive=\"active\" title=\"AI Receptionist\">\n <eds-icon name=\"headset_mic\" [size]=\"22\" [fill]=\"1\" />\n </a>\n <span title=\"Billing (coming soon)\"><eds-icon name=\"currency_exchange\" [size]=\"22\" /></span>\n <span title=\"Documents (coming soon)\"><eds-icon name=\"description\" [size]=\"22\" /></span>\n <span title=\"Leaderboard (coming soon)\"><eds-icon name=\"leaderboard\" [size]=\"22\" /></span>\n </nav>\n <div class=\"eds-rail-footer\">\n <a routerLink=\"settings\" routerLinkActive=\"active\" title=\"Settings\">\n <eds-icon name=\"settings\" [size]=\"22\" />\n </a>\n <span class=\"eds-rail-avatar\" title=\"User\">HS</span>\n </div>\n</aside>\n", styles: [":host{display:block;flex-shrink:0}.eds-rail{width:var(--eds-rail-width);min-height:100vh;background:var(--surface-l0);border-right:1px solid var(--border-default);display:flex;flex-direction:column;align-items:center;padding:12px 0;position:sticky;top:0;height:100vh}.eds-rail-logo{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:var(--eds-radius-md);background:var(--bg-brand);color:#fff;font-family:var(--font-figtree);font-weight:800;font-size:18px;text-decoration:none;margin-bottom:16px}.eds-rail-nav{display:flex;flex-direction:column;gap:4px;flex:1}.eds-rail-nav a,.eds-rail-nav span{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:var(--eds-radius-md);color:var(--gray-500);cursor:default}.eds-rail-nav a{cursor:pointer;text-decoration:none}.eds-rail-nav a:hover{background:var(--gray-100);color:var(--gray-700)}.eds-rail-nav a.active{background:#3681f41f;color:var(--brand-600)}.eds-rail-footer{display:flex;flex-direction:column;align-items:center;gap:8px}.eds-rail-footer a{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:var(--eds-radius-md);color:var(--gray-500);text-decoration:none}.eds-rail-footer a:hover{background:var(--gray-100)}.eds-rail-avatar{width:32px;height:32px;border-radius:50%;background:var(--gray-200);color:var(--gray-600);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center}\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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
82
|
+
}
|
|
83
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppRailComponent, decorators: [{
|
|
84
|
+
type: Component,
|
|
85
|
+
args: [{ selector: 'eds-app-rail', imports: [RouterLink, RouterLinkActive, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<aside class=\"eds-rail\" aria-label=\"EDS product navigation\">\n <a routerLink=\"dashboard\" class=\"eds-rail-logo\" title=\"Edvak\">e</a>\n <nav class=\"eds-rail-nav\">\n <a routerLink=\"dashboard\" routerLinkActive=\"active\" title=\"Dashboard\">\n <eds-icon name=\"space_dashboard\" [size]=\"22\" />\n </a>\n @if (includePracticeNav) {\n <a routerLink=\"appointments\" routerLinkActive=\"active\" title=\"Appointments\">\n <eds-icon name=\"calendar_month\" [size]=\"22\" />\n </a>\n <a routerLink=\"patients\" routerLinkActive=\"active\" title=\"Patients\">\n <eds-icon name=\"groups\" [size]=\"22\" />\n </a>\n }\n <span title=\"Chat (coming soon)\"><eds-icon name=\"chat\" [size]=\"22\" /></span>\n <a routerLink=\"live\" routerLinkActive=\"active\" title=\"AI Receptionist\">\n <eds-icon name=\"headset_mic\" [size]=\"22\" [fill]=\"1\" />\n </a>\n <span title=\"Billing (coming soon)\"><eds-icon name=\"currency_exchange\" [size]=\"22\" /></span>\n <span title=\"Documents (coming soon)\"><eds-icon name=\"description\" [size]=\"22\" /></span>\n <span title=\"Leaderboard (coming soon)\"><eds-icon name=\"leaderboard\" [size]=\"22\" /></span>\n </nav>\n <div class=\"eds-rail-footer\">\n <a routerLink=\"settings\" routerLinkActive=\"active\" title=\"Settings\">\n <eds-icon name=\"settings\" [size]=\"22\" />\n </a>\n <span class=\"eds-rail-avatar\" title=\"User\">HS</span>\n </div>\n</aside>\n", styles: [":host{display:block;flex-shrink:0}.eds-rail{width:var(--eds-rail-width);min-height:100vh;background:var(--surface-l0);border-right:1px solid var(--border-default);display:flex;flex-direction:column;align-items:center;padding:12px 0;position:sticky;top:0;height:100vh}.eds-rail-logo{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:var(--eds-radius-md);background:var(--bg-brand);color:#fff;font-family:var(--font-figtree);font-weight:800;font-size:18px;text-decoration:none;margin-bottom:16px}.eds-rail-nav{display:flex;flex-direction:column;gap:4px;flex:1}.eds-rail-nav a,.eds-rail-nav span{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:var(--eds-radius-md);color:var(--gray-500);cursor:default}.eds-rail-nav a{cursor:pointer;text-decoration:none}.eds-rail-nav a:hover{background:var(--gray-100);color:var(--gray-700)}.eds-rail-nav a.active{background:#3681f41f;color:var(--brand-600)}.eds-rail-footer{display:flex;flex-direction:column;align-items:center;gap:8px}.eds-rail-footer a{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:var(--eds-radius-md);color:var(--gray-500);text-decoration:none}.eds-rail-footer a:hover{background:var(--gray-100)}.eds-rail-avatar{width:32px;height:32px;border-radius:50%;background:var(--gray-200);color:var(--gray-600);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center}\n"] }]
|
|
86
|
+
}] });
|
|
87
|
+
|
|
88
|
+
/** Build URL helpers from a config (or raw base URL). Safe outside injection context. */
|
|
89
|
+
function createVoiceReceptionistPaths(configOrBase) {
|
|
90
|
+
const base = typeof configOrBase === 'string' ? configOrBase : configOrBase.apiBaseUrl;
|
|
91
|
+
const root = base.replace(/\/$/, '');
|
|
92
|
+
return {
|
|
93
|
+
apiRoot: () => root,
|
|
94
|
+
authPath: (suffix = '') => `${root}/api/auth${suffix}`,
|
|
95
|
+
v1Path: (suffix = '') => `${root}/api/v1${suffix}`,
|
|
96
|
+
apiPath: (suffix = '') => `${root}/api${suffix}`,
|
|
97
|
+
aiReceptionistPath: (suffix = '') => `${root}/api/ai-receptionist${suffix}`,
|
|
98
|
+
healthPath: (suffix = '') => `${root}/health${suffix}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Normalize list/detail API payloads into a row array. */
|
|
103
|
+
function extractApiRows(body) {
|
|
104
|
+
if (Array.isArray(body))
|
|
105
|
+
return body;
|
|
106
|
+
if (!body || typeof body !== 'object')
|
|
107
|
+
return [];
|
|
108
|
+
const o = body;
|
|
109
|
+
const list = o['items'] ?? o['patients'] ?? o['appointments'] ?? o['providers'] ?? o['data'];
|
|
110
|
+
if (Array.isArray(list))
|
|
111
|
+
return list;
|
|
112
|
+
if (o['patientId'] || o['patient_id'] || o['appointmentId'] || o['appointment_id']) {
|
|
113
|
+
return [body];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const WELCOME_MESSAGE_MAX = 500;
|
|
119
|
+
function buildAutoWelcomeTemplate(practiceName, agentName) {
|
|
120
|
+
const practice = practiceName.trim() || 'our office';
|
|
121
|
+
const agent = agentName.trim() || 'your AI receptionist';
|
|
122
|
+
return `Hi, thanks for calling ${practice}. This is ${agent}. How can I help you today?`;
|
|
123
|
+
}
|
|
124
|
+
function resolveWelcomePreview(welcomeMessage, practiceName, agentName) {
|
|
125
|
+
const custom = welcomeMessage?.trim();
|
|
126
|
+
if (custom)
|
|
127
|
+
return custom;
|
|
128
|
+
return buildAutoWelcomeTemplate(practiceName, agentName);
|
|
129
|
+
}
|
|
130
|
+
function computeEffectiveWelcomeMessage(settings) {
|
|
131
|
+
const general = settings.general;
|
|
132
|
+
const custom = String(general['welcomeMessage'] ?? '').trim();
|
|
133
|
+
if (custom)
|
|
134
|
+
return custom;
|
|
135
|
+
return buildAutoWelcomeTemplate(String(general['practiceName'] ?? ''), String(general['agentName'] ?? ''));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** V3 agent tool labels — mirrors GET /api/agent-tools/catalog */
|
|
139
|
+
const AGENT_TOOL_LABELS = {
|
|
140
|
+
find_doctors: 'Found Doctors',
|
|
141
|
+
check_doctor_availability: 'Checked Doctor Availability',
|
|
142
|
+
list_visit_types: 'Listed Visit Types',
|
|
143
|
+
search_patients: 'Searched Patients',
|
|
144
|
+
search_practice_faq: 'Searched Practice FAQ',
|
|
145
|
+
book_existing_appointment: 'Booked Appointment',
|
|
146
|
+
register_and_book_appointment: 'Registered & Booked Appointment',
|
|
147
|
+
submit_medication_refill: 'Submitted Medication Refill',
|
|
148
|
+
};
|
|
149
|
+
function looksLikeToolKey(value) {
|
|
150
|
+
return /^[a-z][a-z0-9_]*$/.test(value.trim());
|
|
151
|
+
}
|
|
152
|
+
function humanizeToolKey(key) {
|
|
153
|
+
return key
|
|
154
|
+
.replace(/^tool_/, '')
|
|
155
|
+
.split('_')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
158
|
+
.join(' ');
|
|
159
|
+
}
|
|
160
|
+
/** Prefer API `name`; fall back to catalog label or a readable key. */
|
|
161
|
+
function resolveToolDisplayName(key, name) {
|
|
162
|
+
const trimmed = name?.trim() ?? '';
|
|
163
|
+
if (trimmed && !looksLikeToolKey(trimmed))
|
|
164
|
+
return trimmed;
|
|
165
|
+
const lookupKey = key?.trim() || (looksLikeToolKey(trimmed) ? trimmed : '');
|
|
166
|
+
if (lookupKey && AGENT_TOOL_LABELS[lookupKey])
|
|
167
|
+
return AGENT_TOOL_LABELS[lookupKey];
|
|
168
|
+
if (lookupKey)
|
|
169
|
+
return humanizeToolKey(lookupKey);
|
|
170
|
+
return trimmed || 'Tool activity';
|
|
171
|
+
}
|
|
172
|
+
/** Derive tool key from legacy `tech` values like `tool_check_doctor_availability`. */
|
|
173
|
+
function inferToolKeyFromTech(tech) {
|
|
174
|
+
const trimmed = tech?.trim();
|
|
175
|
+
if (!trimmed)
|
|
176
|
+
return undefined;
|
|
177
|
+
if (trimmed.startsWith('tool_'))
|
|
178
|
+
return trimmed.slice(5);
|
|
179
|
+
if (looksLikeToolKey(trimmed))
|
|
180
|
+
return trimmed;
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
function resolveToolKey(key, name, tech) {
|
|
184
|
+
const explicit = key?.trim();
|
|
185
|
+
if (explicit)
|
|
186
|
+
return explicit;
|
|
187
|
+
if (looksLikeToolKey(name))
|
|
188
|
+
return name.trim();
|
|
189
|
+
return inferToolKeyFromTech(tech);
|
|
190
|
+
}
|
|
191
|
+
/** Stable @for track key — avoid ?? in template track expressions (Angular compiler bug). */
|
|
192
|
+
function toolEventTrackKey(tool, index) {
|
|
193
|
+
return `${tool.key ?? ''}|${tool.t}|${tool.tech}|${tool.name}|${index}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const TONE_COLORS = {
|
|
197
|
+
brand: '#3681F4',
|
|
198
|
+
pos: '#0ABF52',
|
|
199
|
+
notice: '#FFB900',
|
|
200
|
+
neg: '#F03740',
|
|
201
|
+
violet: '#8E51FF',
|
|
202
|
+
gray: '#71717A',
|
|
203
|
+
};
|
|
204
|
+
const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
205
|
+
function parseDurToSeconds(dur) {
|
|
206
|
+
if (!dur)
|
|
207
|
+
return 0;
|
|
208
|
+
const parts = dur.split(':').map((p) => Number(p.trim()));
|
|
209
|
+
if (parts.some((n) => !Number.isFinite(n)))
|
|
210
|
+
return 0;
|
|
211
|
+
if (parts.length === 2)
|
|
212
|
+
return parts[0] * 60 + parts[1];
|
|
213
|
+
if (parts.length === 3)
|
|
214
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
function parsePercent(value) {
|
|
218
|
+
const n = Number(String(value).replace('%', '').trim());
|
|
219
|
+
return Number.isFinite(n) ? n : 0;
|
|
220
|
+
}
|
|
221
|
+
function normalizeConfidence(value) {
|
|
222
|
+
if (value <= 1)
|
|
223
|
+
return Math.round(value * 100);
|
|
224
|
+
return Math.round(value);
|
|
225
|
+
}
|
|
226
|
+
function mapVerification(raw) {
|
|
227
|
+
const v = raw?.toLowerCase();
|
|
228
|
+
if (v === 'verified' || v === 'partial' || v === 'failed' || v === 'unknown')
|
|
229
|
+
return v;
|
|
230
|
+
return 'unknown';
|
|
231
|
+
}
|
|
232
|
+
function mapCallStatus(raw) {
|
|
233
|
+
const s = String(raw ?? 'ai').toLowerCase().replace(/_/g, '-');
|
|
234
|
+
const allowed = [
|
|
235
|
+
'ai',
|
|
236
|
+
'needs-help',
|
|
237
|
+
'human-requested',
|
|
238
|
+
'urgent',
|
|
239
|
+
'takeover',
|
|
240
|
+
'transferring',
|
|
241
|
+
];
|
|
242
|
+
return allowed.includes(s) ? s : 'ai';
|
|
243
|
+
}
|
|
244
|
+
function mapSentiment(raw) {
|
|
245
|
+
const s = String(raw ?? 'neutral').toLowerCase();
|
|
246
|
+
if (s === 'calm' || s === 'neutral' || s === 'frustrated' || s === 'urgent')
|
|
247
|
+
return s;
|
|
248
|
+
return 'neutral';
|
|
249
|
+
}
|
|
250
|
+
function mapTranscriptLine(line) {
|
|
251
|
+
return {
|
|
252
|
+
who: line.who,
|
|
253
|
+
text: line.text,
|
|
254
|
+
flag: line.flag,
|
|
255
|
+
at: line.at ?? line.offsetSeconds ?? line.startTime,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function mapToolEvent(tool) {
|
|
259
|
+
const state = tool.state;
|
|
260
|
+
const key = resolveToolKey(tool.key, tool.name, tool.tech);
|
|
261
|
+
return {
|
|
262
|
+
t: tool.t,
|
|
263
|
+
key,
|
|
264
|
+
name: resolveToolDisplayName(key, tool.name),
|
|
265
|
+
tech: tool.tech,
|
|
266
|
+
result: tool.result,
|
|
267
|
+
state: ['ok', 'warn', 'error', 'active'].includes(state) ? state : 'ok',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function normalizeRecordingStatus(status, url) {
|
|
271
|
+
const known = ['ready', 'pending', 'unavailable', 'failed'];
|
|
272
|
+
if (status && known.includes(status)) {
|
|
273
|
+
return status;
|
|
274
|
+
}
|
|
275
|
+
return url ? 'ready' : 'unavailable';
|
|
276
|
+
}
|
|
277
|
+
function mapRecordingMeta(dto) {
|
|
278
|
+
return {
|
|
279
|
+
callId: dto.callId,
|
|
280
|
+
status: normalizeRecordingStatus(dto.status, dto.url),
|
|
281
|
+
url: dto.url ?? null,
|
|
282
|
+
contentType: dto.contentType,
|
|
283
|
+
durationSeconds: dto.durationSeconds,
|
|
284
|
+
message: dto.message,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function mapLiveCallDto(dto) {
|
|
288
|
+
return {
|
|
289
|
+
id: dto.id,
|
|
290
|
+
name: dto.name,
|
|
291
|
+
initials: dto.initials,
|
|
292
|
+
avatar: dto.avatar,
|
|
293
|
+
phone: dto.phone,
|
|
294
|
+
verification: mapVerification(dto.verification),
|
|
295
|
+
direction: dto.direction,
|
|
296
|
+
reason: dto.reason,
|
|
297
|
+
status: mapCallStatus(dto.status),
|
|
298
|
+
started: dto.started,
|
|
299
|
+
sentiment: mapSentiment(dto.sentiment),
|
|
300
|
+
confidence: normalizeConfidence(dto.confidence),
|
|
301
|
+
goal: dto.goal,
|
|
302
|
+
dob: dto.dob ?? undefined,
|
|
303
|
+
location: dto.location ?? '',
|
|
304
|
+
provider: dto.provider,
|
|
305
|
+
language: dto.language,
|
|
306
|
+
matched: dto.matched,
|
|
307
|
+
assigned: dto.assigned,
|
|
308
|
+
tools: (dto.tools ?? []).map(mapToolEvent),
|
|
309
|
+
transcript: (dto.transcript ?? []).map(mapTranscriptLine),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function mapHistoryCallDto(dto) {
|
|
313
|
+
const parsedStarted = Date.parse(dto.startedAt ?? dto.when);
|
|
314
|
+
return {
|
|
315
|
+
id: dto.id,
|
|
316
|
+
name: dto.name,
|
|
317
|
+
initials: dto.initials,
|
|
318
|
+
phone: dto.phone,
|
|
319
|
+
direction: dto.dir,
|
|
320
|
+
outcome: dto.outcome,
|
|
321
|
+
reason: dto.reason,
|
|
322
|
+
duration: parseDurToSeconds(dto.dur),
|
|
323
|
+
durationLabel: dto.dur,
|
|
324
|
+
started: Number.isFinite(parsedStarted) ? parsedStarted : 0,
|
|
325
|
+
whenLabel: Number.isFinite(parsedStarted) ? undefined : dto.when,
|
|
326
|
+
location: dto.location,
|
|
327
|
+
verification: mapVerification(dto.verify),
|
|
328
|
+
review: dto.review === 'reviewed' ? 'reviewed' : 'needs',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function mapHistoryDetailDto(call, detail, recordingUrl) {
|
|
332
|
+
const base = mapHistoryCallDto(call);
|
|
333
|
+
return {
|
|
334
|
+
...base,
|
|
335
|
+
summary: detail.summary,
|
|
336
|
+
transcript: (detail.transcript ?? []).map(mapTranscriptLine),
|
|
337
|
+
tools: (detail.tools ?? []).map(mapToolEvent),
|
|
338
|
+
recordingUrl: recordingUrl ?? undefined,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function mapAttentionItemDto(dto) {
|
|
342
|
+
return {
|
|
343
|
+
id: dto.id,
|
|
344
|
+
callId: dto.id,
|
|
345
|
+
title: dto.name,
|
|
346
|
+
detail: dto.why || dto.reason,
|
|
347
|
+
severity: dto.sev === 'urgent' ? 'urgent' : 'warn',
|
|
348
|
+
time: Date.now(),
|
|
349
|
+
timeLabel: dto.time,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function mapDashboardSummaryDto(dto) {
|
|
353
|
+
return {
|
|
354
|
+
activeCalls: dto.liveCalls,
|
|
355
|
+
answeredToday: dto.callsToday,
|
|
356
|
+
appointmentsBooked: dto.appointmentsBooked ?? 0,
|
|
357
|
+
tasksCreated: dto.openTasks,
|
|
358
|
+
completionRate: parsePercent(dto.aiHandledRate),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function mapAiCreatedAppointmentDto(dto) {
|
|
362
|
+
return {
|
|
363
|
+
id: dto.id,
|
|
364
|
+
appointmentId: dto.appointmentId ?? dto.id,
|
|
365
|
+
patientName: dto.patientName || 'Unknown patient',
|
|
366
|
+
providerName: dto.providerName ?? null,
|
|
367
|
+
startTime: Date.parse(dto.startTime) || 0,
|
|
368
|
+
createdAt: Date.parse(dto.createdAt) || 0,
|
|
369
|
+
callId: dto.callId ?? null,
|
|
370
|
+
status: dto.status ?? null,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function mapTaskDto(dto) {
|
|
374
|
+
let status = 'open';
|
|
375
|
+
if (dto.status === 'done')
|
|
376
|
+
status = 'done';
|
|
377
|
+
else if (dto.status === 'in-progress')
|
|
378
|
+
status = 'in_progress';
|
|
379
|
+
return {
|
|
380
|
+
id: dto.id,
|
|
381
|
+
created: dto.created ? Date.parse(dto.created) || Date.now() : Date.now(),
|
|
382
|
+
title: dto.title || dto.type || 'Task',
|
|
383
|
+
type: dto.type || 'Other',
|
|
384
|
+
description: dto.summary ?? '',
|
|
385
|
+
patient: dto.patient || '—',
|
|
386
|
+
priority: dto.priority ?? 'normal',
|
|
387
|
+
status,
|
|
388
|
+
source: dto.source ?? 'voice',
|
|
389
|
+
assignee: dto.assignee || '—',
|
|
390
|
+
callId: dto.call || undefined,
|
|
391
|
+
edvakTaskId: dto.edvakTaskId,
|
|
392
|
+
edvakStatus: dto.edvakStatus,
|
|
393
|
+
medicationName: dto.medicationName,
|
|
394
|
+
medicationId: dto.medicationId,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function toApiTaskStatus(status) {
|
|
398
|
+
if (status === 'done')
|
|
399
|
+
return 'done';
|
|
400
|
+
if (status === 'in_progress')
|
|
401
|
+
return 'in-progress';
|
|
402
|
+
return 'open';
|
|
403
|
+
}
|
|
404
|
+
function mapCreateTaskRequest(req) {
|
|
405
|
+
return {
|
|
406
|
+
title: req.title.trim(),
|
|
407
|
+
type: req.type,
|
|
408
|
+
call: req.callId,
|
|
409
|
+
summary: req.description?.trim() || undefined,
|
|
410
|
+
patient: req.patient?.trim() || undefined,
|
|
411
|
+
priority: req.priority,
|
|
412
|
+
assignee: req.assignee?.trim() || undefined,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function mapRegenerateSummaryResponse(dto) {
|
|
416
|
+
return {
|
|
417
|
+
callId: dto.callId,
|
|
418
|
+
summary: dto.summary,
|
|
419
|
+
summaryModel: dto.summaryModel,
|
|
420
|
+
summaryGeneratedAt: dto.summaryGeneratedAt,
|
|
421
|
+
transcriptTurnCount: dto.transcriptTurnCount,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function mapAuditEventDto(dto, index) {
|
|
425
|
+
return {
|
|
426
|
+
id: `${dto.ts}-${index}`,
|
|
427
|
+
actor: dto.actorName || dto.actor,
|
|
428
|
+
action: dto.event,
|
|
429
|
+
target: dto.call || dto.patient || dto.cat,
|
|
430
|
+
detail: dto.detail,
|
|
431
|
+
at: dto.ts ? Date.parse(dto.ts) || Date.now() : Date.now(),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function mapAnalyticsDto(dto, range) {
|
|
435
|
+
const volume = mapVolumeSeries(dto.volume);
|
|
436
|
+
const peakHours = mapPeakSeries(dto.peak);
|
|
437
|
+
const outcomes = (dto.outcomes ?? []).map((o) => ({
|
|
438
|
+
label: o.label,
|
|
439
|
+
value: o.v,
|
|
440
|
+
color: TONE_COLORS[o.tone] ?? TONE_COLORS['brand'],
|
|
441
|
+
}));
|
|
442
|
+
const reasons = (dto.reasons ?? []).map((r) => ({ label: r.label, value: r.v }));
|
|
443
|
+
const transfers = (dto.transfers ?? []).map((t) => ({ label: t.label, value: t.v }));
|
|
444
|
+
const kpiCards = (dto.kpis ?? []).map((k) => ({
|
|
445
|
+
label: k.label,
|
|
446
|
+
value: k.value,
|
|
447
|
+
delta: k.delta,
|
|
448
|
+
tone: k.tone,
|
|
449
|
+
sub: k.sub,
|
|
450
|
+
}));
|
|
451
|
+
const kpiMap = {};
|
|
452
|
+
for (const k of dto.kpis ?? []) {
|
|
453
|
+
const key = k.label.toLowerCase();
|
|
454
|
+
const num = parseAnalyticsNumber(k.value);
|
|
455
|
+
if (key.includes('handled') || key.includes('total call') || key === 'calls') {
|
|
456
|
+
kpiMap['totalCalls'] = num;
|
|
457
|
+
}
|
|
458
|
+
else if (key.includes('handle time') || key.includes('avg duration') || key.includes('duration')) {
|
|
459
|
+
kpiMap['avgDuration'] = parseDurationSeconds(k.value);
|
|
460
|
+
}
|
|
461
|
+
else if (key.includes('resolution') || key.includes('booking')) {
|
|
462
|
+
kpiMap['bookingRate'] = num;
|
|
463
|
+
}
|
|
464
|
+
else if (key.includes('transfer')) {
|
|
465
|
+
kpiMap['transferRate'] = num;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
range,
|
|
470
|
+
kpiCards,
|
|
471
|
+
volume,
|
|
472
|
+
outcomes,
|
|
473
|
+
reasons,
|
|
474
|
+
transfers,
|
|
475
|
+
peakHours,
|
|
476
|
+
kpis: {
|
|
477
|
+
totalCalls: kpiMap['totalCalls'] ?? 0,
|
|
478
|
+
avgDuration: kpiMap['avgDuration'] ?? 0,
|
|
479
|
+
bookingRate: kpiMap['bookingRate'] ?? 0,
|
|
480
|
+
transferRate: kpiMap['transferRate'] ?? 0,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function parseAnalyticsNumber(value) {
|
|
485
|
+
const cleaned = String(value).replace(/[^\d.]/g, '');
|
|
486
|
+
const num = Number(cleaned);
|
|
487
|
+
return Number.isFinite(num) ? num : 0;
|
|
488
|
+
}
|
|
489
|
+
function parseDurationSeconds(value) {
|
|
490
|
+
const trimmed = value.trim();
|
|
491
|
+
const clock = /^(\d+):(\d{2})$/.exec(trimmed);
|
|
492
|
+
if (clock) {
|
|
493
|
+
return Number(clock[1]) * 60 + Number(clock[2]);
|
|
494
|
+
}
|
|
495
|
+
return parseAnalyticsNumber(trimmed);
|
|
496
|
+
}
|
|
497
|
+
function isVolumeSeriesDto(raw) {
|
|
498
|
+
return !!raw && typeof raw === 'object' && !Array.isArray(raw) && 'points' in raw;
|
|
499
|
+
}
|
|
500
|
+
function isPeakSeriesDto(raw) {
|
|
501
|
+
return !!raw && typeof raw === 'object' && !Array.isArray(raw) && 'points' in raw;
|
|
502
|
+
}
|
|
503
|
+
function formatLegacyHourLabel(hour) {
|
|
504
|
+
if (hour === 0)
|
|
505
|
+
return '12 AM';
|
|
506
|
+
if (hour < 12)
|
|
507
|
+
return `${hour} AM`;
|
|
508
|
+
if (hour === 12)
|
|
509
|
+
return '12 PM';
|
|
510
|
+
return `${hour - 12} PM`;
|
|
511
|
+
}
|
|
512
|
+
function mapVolumeSeries(raw) {
|
|
513
|
+
if (isVolumeSeriesDto(raw)) {
|
|
514
|
+
return {
|
|
515
|
+
timezone: raw.timezone ?? 'UTC',
|
|
516
|
+
startDate: raw.startDate ?? '',
|
|
517
|
+
endDate: raw.endDate ?? '',
|
|
518
|
+
points: (raw.points ?? []).map((p) => ({
|
|
519
|
+
date: p.date,
|
|
520
|
+
day: p.day,
|
|
521
|
+
label: p.label,
|
|
522
|
+
count: p.count,
|
|
523
|
+
})),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const values = Array.isArray(raw) ? raw : [];
|
|
527
|
+
return {
|
|
528
|
+
timezone: 'UTC',
|
|
529
|
+
startDate: '',
|
|
530
|
+
endDate: '',
|
|
531
|
+
points: values.map((count, i) => ({
|
|
532
|
+
date: '',
|
|
533
|
+
day: DAY_LABELS[i % DAY_LABELS.length] ?? `D${i + 1}`,
|
|
534
|
+
label: DAY_LABELS[i % DAY_LABELS.length] ?? `Day ${i + 1}`,
|
|
535
|
+
count,
|
|
536
|
+
})),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function mapPeakSeries(raw) {
|
|
540
|
+
if (isPeakSeriesDto(raw)) {
|
|
541
|
+
return {
|
|
542
|
+
timezone: raw.timezone ?? 'UTC',
|
|
543
|
+
points: (raw.points ?? []).map((p) => ({
|
|
544
|
+
hour: p.hour,
|
|
545
|
+
label: p.label,
|
|
546
|
+
count: p.count,
|
|
547
|
+
})),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const values = Array.isArray(raw) ? raw : [];
|
|
551
|
+
return {
|
|
552
|
+
timezone: 'UTC',
|
|
553
|
+
points: values.map((count, hour) => ({
|
|
554
|
+
hour,
|
|
555
|
+
label: formatLegacyHourLabel(hour),
|
|
556
|
+
count,
|
|
557
|
+
})),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function mapStatusDto(dto) {
|
|
561
|
+
const online = dto.agentOnline ?? dto.activated ?? false;
|
|
562
|
+
const step = dto.setupStep ?? (dto.activated ? 11 : 0);
|
|
563
|
+
return {
|
|
564
|
+
agentOn: online,
|
|
565
|
+
mode: 'live',
|
|
566
|
+
readiness: Math.min(100, Math.round((step / 11) * 100)),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function mapSetupDto(dto) {
|
|
570
|
+
const stepCount = 11;
|
|
571
|
+
const steps = Array.from({ length: stepCount }, (_, i) => ({
|
|
572
|
+
id: i + 1,
|
|
573
|
+
title: `Setup step ${i + 1}`,
|
|
574
|
+
description: dto.activated ? 'Complete' : i + 1 < dto.step ? 'Complete' : 'Pending',
|
|
575
|
+
complete: dto.activated || i + 1 < dto.step,
|
|
576
|
+
}));
|
|
577
|
+
return {
|
|
578
|
+
readiness: Math.min(100, Math.round((dto.step / stepCount) * 100)),
|
|
579
|
+
steps,
|
|
580
|
+
testTranscript: [],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function mapSettingsResponse(body, safetyBody) {
|
|
584
|
+
const raw = body;
|
|
585
|
+
const settings = raw?.settings ?? body ?? {};
|
|
586
|
+
const section = (key) => {
|
|
587
|
+
const val = settings[key];
|
|
588
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
589
|
+
return val;
|
|
590
|
+
}
|
|
591
|
+
return {};
|
|
592
|
+
};
|
|
593
|
+
const safetyRaw = safetyBody;
|
|
594
|
+
const safety = (safetyRaw?.checks ?? []).map((c) => ({
|
|
595
|
+
id: c.id,
|
|
596
|
+
label: c.label ?? humanizeSettingsKey(c.id),
|
|
597
|
+
status: c.ok ? 'ok' : 'warn',
|
|
598
|
+
}));
|
|
599
|
+
const mapped = {
|
|
600
|
+
general: section('general'),
|
|
601
|
+
phone: section('phoneAvailability'),
|
|
602
|
+
tasks: section('allowedTasks'),
|
|
603
|
+
scheduling: section('schedulingRules'),
|
|
604
|
+
refill: section('refillRules'),
|
|
605
|
+
verify: section('patientVerification'),
|
|
606
|
+
escalation: section('humanEscalation'),
|
|
607
|
+
outbound: section('outboundCalling'),
|
|
608
|
+
safety,
|
|
609
|
+
transcripts: section('transcriptsRecording'),
|
|
610
|
+
advanced: section('advanced'),
|
|
611
|
+
};
|
|
612
|
+
mapped.effectiveWelcomeMessage =
|
|
613
|
+
raw.effectiveWelcomeMessage ?? computeEffectiveWelcomeMessage(mapped);
|
|
614
|
+
return mapped;
|
|
615
|
+
}
|
|
616
|
+
function humanizeSettingsKey(key) {
|
|
617
|
+
return key
|
|
618
|
+
.replace(/([A-Z])/g, ' $1')
|
|
619
|
+
.replace(/_/g, ' ')
|
|
620
|
+
.trim()
|
|
621
|
+
.replace(/^\w/, (c) => c.toUpperCase());
|
|
622
|
+
}
|
|
623
|
+
/** Map frontend AgentSettings → backend PUT body section keys. */
|
|
624
|
+
function mapSettingsToRequest(settings) {
|
|
625
|
+
return {
|
|
626
|
+
general: settings.general,
|
|
627
|
+
phoneAvailability: settings.phone,
|
|
628
|
+
allowedTasks: settings.tasks,
|
|
629
|
+
schedulingRules: settings.scheduling,
|
|
630
|
+
refillRules: settings.refill,
|
|
631
|
+
patientVerification: settings.verify,
|
|
632
|
+
humanEscalation: settings.escalation,
|
|
633
|
+
outboundCalling: settings.outbound,
|
|
634
|
+
transcriptsRecording: settings.transcripts,
|
|
635
|
+
advanced: settings.advanced,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function mapLocationDto(raw) {
|
|
639
|
+
const r = raw;
|
|
640
|
+
return {
|
|
641
|
+
id: r.locationId ?? r.id ?? '',
|
|
642
|
+
name: r.locationName ?? r.name ?? '',
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function mapProviderDto(raw) {
|
|
646
|
+
const r = raw;
|
|
647
|
+
return {
|
|
648
|
+
id: r.providerId ?? r.id ?? '',
|
|
649
|
+
name: r.fullName ?? r.name ?? '',
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function asTranscriptList(body) {
|
|
653
|
+
if (Array.isArray(body))
|
|
654
|
+
return body;
|
|
655
|
+
if (!body || typeof body !== 'object')
|
|
656
|
+
return [];
|
|
657
|
+
const o = body;
|
|
658
|
+
const list = o['lines'] ?? o['transcript'] ?? o['items'] ?? o['data'];
|
|
659
|
+
return Array.isArray(list) ? list : [];
|
|
660
|
+
}
|
|
661
|
+
function isHistoryCallDto(raw) {
|
|
662
|
+
return !!raw && typeof raw === 'object' && 'dir' in raw;
|
|
663
|
+
}
|
|
664
|
+
function asHistoryList(body) {
|
|
665
|
+
if (Array.isArray(body))
|
|
666
|
+
return body;
|
|
667
|
+
const list = body;
|
|
668
|
+
return list?.items ?? [];
|
|
669
|
+
}
|
|
670
|
+
function asLiveCallList(body) {
|
|
671
|
+
if (Array.isArray(body))
|
|
672
|
+
return body;
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
function asTaskList(body) {
|
|
676
|
+
if (Array.isArray(body))
|
|
677
|
+
return body;
|
|
678
|
+
const list = body;
|
|
679
|
+
return list?.items ?? [];
|
|
680
|
+
}
|
|
681
|
+
function asAuditList(body) {
|
|
682
|
+
if (Array.isArray(body))
|
|
683
|
+
return body;
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
function asAttentionList(body) {
|
|
687
|
+
if (Array.isArray(body))
|
|
688
|
+
return body;
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const now$1 = Date.now();
|
|
693
|
+
function item(partial) {
|
|
694
|
+
return {
|
|
695
|
+
callId: 'LC-4821',
|
|
696
|
+
method: 'POST',
|
|
697
|
+
urlPath: '/api/v1/patients/search',
|
|
698
|
+
statusCode: 200,
|
|
699
|
+
success: true,
|
|
700
|
+
durationMs: 340,
|
|
701
|
+
toolName: null,
|
|
702
|
+
errorMessage: null,
|
|
703
|
+
startedAt: new Date(now$1 - 120_000).toISOString(),
|
|
704
|
+
requestPreview: '{"phone":"+15551234567"}',
|
|
705
|
+
responsePreview: '{"items":[{"id":"p1"}]}',
|
|
706
|
+
...partial,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const SEED_CALL_API_LOGS = {
|
|
710
|
+
'LC-4821': [
|
|
711
|
+
item({
|
|
712
|
+
id: 'log-1',
|
|
713
|
+
provider: 'vonage',
|
|
714
|
+
operation: 'answer_call',
|
|
715
|
+
method: 'POST',
|
|
716
|
+
urlPath: '/vonage/answer',
|
|
717
|
+
statusCode: 200,
|
|
718
|
+
durationMs: 120,
|
|
719
|
+
startedAt: new Date(now$1 - 300_000).toISOString(),
|
|
720
|
+
requestPreview: '{"callId":"..."}',
|
|
721
|
+
responsePreview: '{"status":"connected"}',
|
|
722
|
+
}),
|
|
723
|
+
item({
|
|
724
|
+
id: 'log-2',
|
|
725
|
+
provider: 'edvak',
|
|
726
|
+
operation: 'search_patient',
|
|
727
|
+
toolName: 'search_patient',
|
|
728
|
+
method: 'GET',
|
|
729
|
+
urlPath: '/api/v1/patients',
|
|
730
|
+
statusCode: 200,
|
|
731
|
+
durationMs: 420,
|
|
732
|
+
startedAt: new Date(now$1 - 240_000).toISOString(),
|
|
733
|
+
}),
|
|
734
|
+
item({
|
|
735
|
+
id: 'log-3',
|
|
736
|
+
provider: 'openai',
|
|
737
|
+
operation: 'chat_completion',
|
|
738
|
+
method: 'POST',
|
|
739
|
+
urlPath: '/v1/chat/completions',
|
|
740
|
+
statusCode: 200,
|
|
741
|
+
durationMs: 890,
|
|
742
|
+
startedAt: new Date(now$1 - 180_000).toISOString(),
|
|
743
|
+
requestPreview: '{"model":"gpt-4o-mini"}',
|
|
744
|
+
responsePreview: '{"choices":[...]}',
|
|
745
|
+
}),
|
|
746
|
+
item({
|
|
747
|
+
id: 'log-4',
|
|
748
|
+
provider: 'edvak',
|
|
749
|
+
operation: 'create_appointment',
|
|
750
|
+
toolName: 'create_appointment',
|
|
751
|
+
method: 'POST',
|
|
752
|
+
urlPath: '/api/appointments',
|
|
753
|
+
statusCode: 201,
|
|
754
|
+
durationMs: 560,
|
|
755
|
+
startedAt: new Date(now$1 - 60_000).toISOString(),
|
|
756
|
+
}),
|
|
757
|
+
],
|
|
758
|
+
'HC-9001': [
|
|
759
|
+
item({
|
|
760
|
+
id: 'log-f1',
|
|
761
|
+
callId: 'HC-9001',
|
|
762
|
+
provider: 'edvak',
|
|
763
|
+
operation: 'search_patient',
|
|
764
|
+
toolName: 'search_patient',
|
|
765
|
+
statusCode: 500,
|
|
766
|
+
success: false,
|
|
767
|
+
durationMs: 2100,
|
|
768
|
+
errorMessage: 'Internal Server Error',
|
|
769
|
+
startedAt: new Date(now$1 - 86_400_000).toISOString(),
|
|
770
|
+
}),
|
|
771
|
+
],
|
|
772
|
+
};
|
|
773
|
+
const SEED_API_LOG_DETAILS = {
|
|
774
|
+
'log-4': {
|
|
775
|
+
...SEED_CALL_API_LOGS['LC-4821'][3],
|
|
776
|
+
traceId: 'trace-abc-123',
|
|
777
|
+
requestBody: { patientId: 'p1', slotId: 'slot-9', reason: 'Annual checkup' },
|
|
778
|
+
responseBody: { appointmentId: 'apt-42', status: 'confirmed' },
|
|
779
|
+
metadata: { practiceId: 'prac-1', locationId: 'loc-1' },
|
|
780
|
+
completedAt: new Date(now$1 - 59_500).toISOString(),
|
|
781
|
+
},
|
|
782
|
+
'log-f1': {
|
|
783
|
+
...SEED_CALL_API_LOGS['HC-9001'][0],
|
|
784
|
+
traceId: 'trace-err-999',
|
|
785
|
+
requestBody: { phone: '+15559876543' },
|
|
786
|
+
responseBody: { error: 'Internal Server Error', message: 'Database timeout' },
|
|
787
|
+
metadata: { retryCount: 1 },
|
|
788
|
+
completedAt: new Date(now$1 - 86_398_000).toISOString(),
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
function allSeedLogs() {
|
|
792
|
+
return Object.values(SEED_CALL_API_LOGS).flat();
|
|
793
|
+
}
|
|
794
|
+
const SEED_API_LOG_STATS = {
|
|
795
|
+
last24h: { total: 128, failed: 4, avgDurationMs: 680 },
|
|
796
|
+
byProvider: [
|
|
797
|
+
{ provider: 'edvak', total: 52, failed: 3 },
|
|
798
|
+
{ provider: 'openai', total: 38, failed: 0 },
|
|
799
|
+
{ provider: 'vonage', total: 24, failed: 1 },
|
|
800
|
+
{ provider: 'livekit', total: 14, failed: 0 },
|
|
801
|
+
],
|
|
802
|
+
recentFailures: [
|
|
803
|
+
item({
|
|
804
|
+
id: 'log-fail-1',
|
|
805
|
+
callId: 'HC-9001',
|
|
806
|
+
provider: 'edvak',
|
|
807
|
+
operation: 'search_patient',
|
|
808
|
+
statusCode: 500,
|
|
809
|
+
success: false,
|
|
810
|
+
durationMs: 2100,
|
|
811
|
+
errorMessage: 'Internal Server Error',
|
|
812
|
+
startedAt: new Date(now$1 - 3_600_000).toISOString(),
|
|
813
|
+
}),
|
|
814
|
+
item({
|
|
815
|
+
id: 'log-fail-2',
|
|
816
|
+
callId: 'LC-4819',
|
|
817
|
+
provider: 'vonage',
|
|
818
|
+
operation: 'transfer',
|
|
819
|
+
statusCode: 503,
|
|
820
|
+
success: false,
|
|
821
|
+
durationMs: 1500,
|
|
822
|
+
errorMessage: 'Service unavailable',
|
|
823
|
+
startedAt: new Date(now$1 - 7_200_000).toISOString(),
|
|
824
|
+
}),
|
|
825
|
+
item({
|
|
826
|
+
id: 'log-fail-3',
|
|
827
|
+
callId: null,
|
|
828
|
+
provider: 'edvak',
|
|
829
|
+
operation: 'list_providers',
|
|
830
|
+
statusCode: 500,
|
|
831
|
+
success: false,
|
|
832
|
+
durationMs: 980,
|
|
833
|
+
errorMessage: 'Gateway timeout',
|
|
834
|
+
startedAt: new Date(now$1 - 10_800_000).toISOString(),
|
|
835
|
+
}),
|
|
836
|
+
],
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
const min = (m) => now - m * 60_000;
|
|
841
|
+
const baseTranscript = [
|
|
842
|
+
{ who: 'system', text: 'Call connected · inbound PSTN' },
|
|
843
|
+
{
|
|
844
|
+
who: 'ai',
|
|
845
|
+
text: 'Good afternoon, thank you for calling Riverside Family Medicine. How can I help you today?',
|
|
846
|
+
},
|
|
847
|
+
{ who: 'patient', text: "Hi, I'd like to reschedule my appointment for next Tuesday." },
|
|
848
|
+
{
|
|
849
|
+
who: 'ai',
|
|
850
|
+
text: 'I can help with that. May I verify your date of birth and the name on the account?',
|
|
851
|
+
flag: 'verify',
|
|
852
|
+
},
|
|
853
|
+
{ who: 'patient', text: 'March 14, 1985 — Maria Santos.' },
|
|
854
|
+
{ who: 'ai', text: 'Thank you, Maria. I found your chart. You have a visit with Dr. Chen on Tuesday at 2:30 PM.' },
|
|
855
|
+
{ who: 'patient', text: 'Can we move it to Thursday morning instead?' },
|
|
856
|
+
{ who: 'ai', text: 'Checking availability with scheduling…' },
|
|
857
|
+
];
|
|
858
|
+
const streamingLines = [
|
|
859
|
+
{ who: 'ai', text: 'I have Thursday at 9:15 AM or 10:40 AM with Dr. Chen. Which works better?' },
|
|
860
|
+
{ who: 'patient', text: '9:15 is perfect.' },
|
|
861
|
+
{ who: 'ai', text: 'Great — I’ve moved your appointment to Thursday at 9:15 AM. You’ll receive a confirmation text shortly.' },
|
|
862
|
+
];
|
|
863
|
+
const SEED_LIVE_CALLS = [
|
|
864
|
+
{
|
|
865
|
+
id: 'LC-4821',
|
|
866
|
+
name: 'Maria Santos',
|
|
867
|
+
initials: 'MS',
|
|
868
|
+
avatar: null,
|
|
869
|
+
phone: '+1 (512) 555-0142',
|
|
870
|
+
verification: 'verified',
|
|
871
|
+
direction: 'inbound',
|
|
872
|
+
reason: 'Reschedule appointment',
|
|
873
|
+
status: 'ai',
|
|
874
|
+
started: min(4),
|
|
875
|
+
sentiment: 'calm',
|
|
876
|
+
confidence: 92,
|
|
877
|
+
goal: 'Reschedule visit with Dr. Chen',
|
|
878
|
+
dob: '1985-03-14',
|
|
879
|
+
location: 'Riverside Main',
|
|
880
|
+
provider: 'Dr. Elena Chen',
|
|
881
|
+
language: 'English',
|
|
882
|
+
matched: true,
|
|
883
|
+
assigned: null,
|
|
884
|
+
tools: [
|
|
885
|
+
{
|
|
886
|
+
t: '0:42',
|
|
887
|
+
key: 'search_patients',
|
|
888
|
+
name: 'Searched Patients',
|
|
889
|
+
tech: 'tool_search_patients',
|
|
890
|
+
result: 'Matched DOB + name',
|
|
891
|
+
state: 'ok',
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
t: '1:18',
|
|
895
|
+
key: 'check_doctor_availability',
|
|
896
|
+
name: 'Checked Doctor Availability',
|
|
897
|
+
tech: 'tool_check_doctor_availability',
|
|
898
|
+
result: '1 upcoming visit',
|
|
899
|
+
state: 'ok',
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
t: '2:05',
|
|
903
|
+
key: 'check_doctor_availability',
|
|
904
|
+
name: 'Checked Doctor Availability',
|
|
905
|
+
tech: 'tool_check_doctor_availability',
|
|
906
|
+
result: '2 slots found',
|
|
907
|
+
state: 'active',
|
|
908
|
+
},
|
|
909
|
+
],
|
|
910
|
+
transcript: [...baseTranscript],
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
id: 'LC-4819',
|
|
914
|
+
name: 'James Okonkwo',
|
|
915
|
+
initials: 'JO',
|
|
916
|
+
avatar: null,
|
|
917
|
+
phone: '+1 (737) 555-0198',
|
|
918
|
+
verification: 'partial',
|
|
919
|
+
direction: 'inbound',
|
|
920
|
+
reason: 'Medication refill — urgent',
|
|
921
|
+
status: 'needs-help',
|
|
922
|
+
started: min(7),
|
|
923
|
+
sentiment: 'frustrated',
|
|
924
|
+
confidence: 71,
|
|
925
|
+
goal: 'Refill lisinopril 10mg',
|
|
926
|
+
location: 'North Clinic',
|
|
927
|
+
provider: 'Dr. Patel',
|
|
928
|
+
language: 'English',
|
|
929
|
+
matched: true,
|
|
930
|
+
assigned: 'Front desk queue',
|
|
931
|
+
tools: [
|
|
932
|
+
{
|
|
933
|
+
t: '0:55',
|
|
934
|
+
key: 'search_patients',
|
|
935
|
+
name: 'Searched Patients',
|
|
936
|
+
tech: 'tool_search_patients',
|
|
937
|
+
result: 'Partial match',
|
|
938
|
+
state: 'warn',
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
t: '1:40',
|
|
942
|
+
key: 'submit_medication_refill',
|
|
943
|
+
name: 'Submitted Medication Refill',
|
|
944
|
+
tech: 'tool_submit_medication_refill',
|
|
945
|
+
result: 'Requires staff review',
|
|
946
|
+
state: 'warn',
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
transcript: [
|
|
950
|
+
{ who: 'ai', text: 'Thank you for calling North Clinic. How may I assist you?' },
|
|
951
|
+
{ who: 'patient', text: 'I need my blood pressure medication refilled today — I’m out of pills.' },
|
|
952
|
+
{ who: 'ai', text: 'I understand. Let me pull up your chart and check refill eligibility.', flag: 'verify' },
|
|
953
|
+
{ who: 'patient', text: "I've been a patient for years, this shouldn't be so hard." },
|
|
954
|
+
{ who: 'system', text: 'Escalation suggested — sentiment frustrated', flag: 'escalate' },
|
|
955
|
+
],
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
id: 'LC-4815',
|
|
959
|
+
name: 'Unknown caller',
|
|
960
|
+
initials: '?',
|
|
961
|
+
avatar: null,
|
|
962
|
+
phone: '+1 (512) 555-0881',
|
|
963
|
+
verification: 'unknown',
|
|
964
|
+
direction: 'inbound',
|
|
965
|
+
reason: 'Billing question',
|
|
966
|
+
status: 'human-requested',
|
|
967
|
+
started: min(2),
|
|
968
|
+
sentiment: 'neutral',
|
|
969
|
+
confidence: 58,
|
|
970
|
+
goal: 'Speak with billing',
|
|
971
|
+
location: 'Riverside Main',
|
|
972
|
+
provider: null,
|
|
973
|
+
language: 'English',
|
|
974
|
+
matched: false,
|
|
975
|
+
assigned: null,
|
|
976
|
+
tools: [
|
|
977
|
+
{
|
|
978
|
+
t: '0:30',
|
|
979
|
+
key: 'search_practice_faq',
|
|
980
|
+
name: 'Searched Practice FAQ',
|
|
981
|
+
tech: 'tool_search_practice_faq',
|
|
982
|
+
result: 'Billing / human handoff',
|
|
983
|
+
state: 'ok',
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
transcript: [
|
|
987
|
+
{ who: 'ai', text: 'Good afternoon. How can I help you today?' },
|
|
988
|
+
{ who: 'patient', text: 'I need to talk to someone in billing about my statement.' },
|
|
989
|
+
{ who: 'ai', text: 'I can connect you with our billing team. One moment while I transfer you.' },
|
|
990
|
+
],
|
|
991
|
+
},
|
|
992
|
+
];
|
|
993
|
+
const SEED_ATTENTION = [
|
|
994
|
+
{
|
|
995
|
+
id: 'ATT-1',
|
|
996
|
+
callId: 'LC-4819',
|
|
997
|
+
title: 'Refill needs staff approval',
|
|
998
|
+
detail: 'Patient out of medication — partial verification',
|
|
999
|
+
severity: 'urgent',
|
|
1000
|
+
time: min(1),
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
id: 'ATT-2',
|
|
1004
|
+
callId: 'LC-4815',
|
|
1005
|
+
title: 'Human requested',
|
|
1006
|
+
detail: 'Billing transfer pending',
|
|
1007
|
+
severity: 'warn',
|
|
1008
|
+
time: min(0.5),
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
id: 'ATT-3',
|
|
1012
|
+
callId: 'LC-4821',
|
|
1013
|
+
title: 'Scheduling in progress',
|
|
1014
|
+
detail: 'AI handling — monitor recommended',
|
|
1015
|
+
severity: 'info',
|
|
1016
|
+
time: min(3),
|
|
1017
|
+
},
|
|
1018
|
+
];
|
|
1019
|
+
const SEED_HISTORY = [
|
|
1020
|
+
{
|
|
1021
|
+
id: 'HC-9001',
|
|
1022
|
+
name: 'Linda Nguyen',
|
|
1023
|
+
initials: 'LN',
|
|
1024
|
+
phone: '+1 (512) 555-0211',
|
|
1025
|
+
direction: 'inbound',
|
|
1026
|
+
outcome: 'Appointment booked',
|
|
1027
|
+
reason: 'New patient intake',
|
|
1028
|
+
duration: 342,
|
|
1029
|
+
started: min(45),
|
|
1030
|
+
location: 'Riverside Main',
|
|
1031
|
+
verification: 'verified',
|
|
1032
|
+
review: 'reviewed',
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'HC-9002',
|
|
1036
|
+
name: 'Robert Kim',
|
|
1037
|
+
initials: 'RK',
|
|
1038
|
+
phone: '+1 (737) 555-0333',
|
|
1039
|
+
direction: 'outbound',
|
|
1040
|
+
outcome: 'Voicemail',
|
|
1041
|
+
reason: 'Follow-up reminder',
|
|
1042
|
+
duration: 28,
|
|
1043
|
+
started: min(90),
|
|
1044
|
+
location: 'North Clinic',
|
|
1045
|
+
verification: 'verified',
|
|
1046
|
+
review: 'needs',
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
id: 'HC-9003',
|
|
1050
|
+
name: 'Sarah Williams',
|
|
1051
|
+
initials: 'SW',
|
|
1052
|
+
phone: '+1 (512) 555-0444',
|
|
1053
|
+
direction: 'inbound',
|
|
1054
|
+
outcome: 'Transferred',
|
|
1055
|
+
reason: 'Insurance prior auth',
|
|
1056
|
+
duration: 198,
|
|
1057
|
+
started: min(120),
|
|
1058
|
+
location: 'Riverside Main',
|
|
1059
|
+
verification: 'verified',
|
|
1060
|
+
review: 'needs',
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
id: 'HC-9004',
|
|
1064
|
+
name: 'Carlos Mendez',
|
|
1065
|
+
initials: 'CM',
|
|
1066
|
+
phone: '+1 (512) 555-0555',
|
|
1067
|
+
direction: 'inbound',
|
|
1068
|
+
outcome: 'Task created',
|
|
1069
|
+
reason: 'Lab results question',
|
|
1070
|
+
duration: 156,
|
|
1071
|
+
started: min(180),
|
|
1072
|
+
location: 'South Clinic',
|
|
1073
|
+
verification: 'partial',
|
|
1074
|
+
review: 'reviewed',
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
id: 'HC-9005',
|
|
1078
|
+
name: 'Emily Foster',
|
|
1079
|
+
initials: 'EF',
|
|
1080
|
+
phone: '+1 (737) 555-0666',
|
|
1081
|
+
direction: 'inbound',
|
|
1082
|
+
outcome: 'Completed',
|
|
1083
|
+
reason: 'Hours & directions',
|
|
1084
|
+
duration: 89,
|
|
1085
|
+
started: min(240),
|
|
1086
|
+
location: 'North Clinic',
|
|
1087
|
+
verification: 'unknown',
|
|
1088
|
+
review: 'reviewed',
|
|
1089
|
+
},
|
|
1090
|
+
];
|
|
1091
|
+
const SEED_HISTORY_DETAILS = {
|
|
1092
|
+
'HC-9001': {
|
|
1093
|
+
...SEED_HISTORY[0],
|
|
1094
|
+
summary: 'New patient intake completed. AI collected demographics, verified insurance, and booked initial visit with Dr. Chen for next Monday at 10:00 AM.',
|
|
1095
|
+
transcript: [
|
|
1096
|
+
{ who: 'ai', text: 'Welcome to Riverside Family Medicine. Are you a new patient?', at: 8 },
|
|
1097
|
+
{ who: 'patient', text: 'Yes, I need to establish care.', at: 22 },
|
|
1098
|
+
{ who: 'ai', text: 'I can help schedule your first visit. May I collect a few details?', at: 38 },
|
|
1099
|
+
{ who: 'patient', text: 'Sure.', at: 62 },
|
|
1100
|
+
{ who: 'ai', text: 'Booked: Mon 10:00 AM with Dr. Chen. Confirmation sent via SMS.', at: 285 },
|
|
1101
|
+
],
|
|
1102
|
+
tools: [
|
|
1103
|
+
{
|
|
1104
|
+
t: '1:02',
|
|
1105
|
+
key: 'register_and_book_appointment',
|
|
1106
|
+
name: 'Registered & Booked Appointment',
|
|
1107
|
+
tech: 'tool_register_and_book_appointment',
|
|
1108
|
+
result: 'Chart created',
|
|
1109
|
+
state: 'ok',
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
t: '3:45',
|
|
1113
|
+
key: 'book_existing_appointment',
|
|
1114
|
+
name: 'Booked Appointment',
|
|
1115
|
+
tech: 'tool_book_existing_appointment',
|
|
1116
|
+
result: 'Slot reserved',
|
|
1117
|
+
state: 'ok',
|
|
1118
|
+
},
|
|
1119
|
+
],
|
|
1120
|
+
recordingUrl: '#mock-recording',
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
const SEED_TASKS = [
|
|
1124
|
+
{
|
|
1125
|
+
id: 'TK-101',
|
|
1126
|
+
created: min(180),
|
|
1127
|
+
title: 'Refill: Lisinopril 10mg',
|
|
1128
|
+
type: 'Refill request',
|
|
1129
|
+
description: 'Patient requested refill for lisinopril 10mg. Last filled 28 days ago.',
|
|
1130
|
+
patient: 'James Okonkwo',
|
|
1131
|
+
priority: 'urgent',
|
|
1132
|
+
status: 'open',
|
|
1133
|
+
source: 'voice',
|
|
1134
|
+
assignee: 'Clinical staff',
|
|
1135
|
+
callId: 'HC-9001',
|
|
1136
|
+
edvakTaskId: 'EDV-8821',
|
|
1137
|
+
edvakStatus: 'Pending review',
|
|
1138
|
+
medicationName: 'Lisinopril 10mg',
|
|
1139
|
+
medicationId: 'RX-4492',
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
id: 'TK-102',
|
|
1143
|
+
created: min(240),
|
|
1144
|
+
title: 'Billing follow-up',
|
|
1145
|
+
type: 'Billing',
|
|
1146
|
+
description: 'Caller disputed a statement charge from March visit. Needs billing review.',
|
|
1147
|
+
patient: 'Unknown caller',
|
|
1148
|
+
priority: 'normal',
|
|
1149
|
+
status: 'open',
|
|
1150
|
+
source: 'voice',
|
|
1151
|
+
assignee: 'Billing',
|
|
1152
|
+
callId: 'LC-4815',
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
id: 'TK-103',
|
|
1156
|
+
created: min(300),
|
|
1157
|
+
title: 'Callback requested',
|
|
1158
|
+
type: 'Callback',
|
|
1159
|
+
description: 'Sarah Williams asked for a nurse callback regarding prior auth status.',
|
|
1160
|
+
patient: 'Sarah Williams',
|
|
1161
|
+
priority: 'normal',
|
|
1162
|
+
status: 'in_progress',
|
|
1163
|
+
source: 'voice',
|
|
1164
|
+
assignee: 'Front desk',
|
|
1165
|
+
callId: 'HC-9003',
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
id: 'TK-104',
|
|
1169
|
+
created: min(360),
|
|
1170
|
+
title: 'Callback — lab results',
|
|
1171
|
+
type: 'Callback',
|
|
1172
|
+
description: 'Carlos Mendez requested nurse callback about recent lab results.',
|
|
1173
|
+
patient: 'Carlos Mendez',
|
|
1174
|
+
priority: 'normal',
|
|
1175
|
+
status: 'done',
|
|
1176
|
+
source: 'sync',
|
|
1177
|
+
assignee: 'Nurse line',
|
|
1178
|
+
callId: 'HC-9004',
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
id: 'TK-105',
|
|
1182
|
+
created: min(90),
|
|
1183
|
+
title: 'Schedule follow-up visit',
|
|
1184
|
+
type: 'Scheduling',
|
|
1185
|
+
description: 'Patient needs a 2-week follow-up after ER visit.',
|
|
1186
|
+
patient: 'Maria Chen',
|
|
1187
|
+
priority: 'normal',
|
|
1188
|
+
status: 'open',
|
|
1189
|
+
source: 'manual',
|
|
1190
|
+
assignee: 'Scheduling',
|
|
1191
|
+
},
|
|
1192
|
+
];
|
|
1193
|
+
const SEED_AUDIT = [
|
|
1194
|
+
{
|
|
1195
|
+
id: 'AU-1',
|
|
1196
|
+
actor: 'admin@riverside.fm',
|
|
1197
|
+
action: 'Updated welcome message',
|
|
1198
|
+
target: 'Agent Settings',
|
|
1199
|
+
detail: 'Changed inbound greeting',
|
|
1200
|
+
at: min(30),
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: 'AU-2',
|
|
1204
|
+
actor: 'system',
|
|
1205
|
+
action: 'Agent activated',
|
|
1206
|
+
target: 'AI Receptionist',
|
|
1207
|
+
detail: 'After-hours mode enabled',
|
|
1208
|
+
at: min(120),
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
id: 'AU-3',
|
|
1212
|
+
actor: 'staff@riverside.fm',
|
|
1213
|
+
action: 'Takeover',
|
|
1214
|
+
target: 'LC-4780',
|
|
1215
|
+
detail: 'Staff joined live call',
|
|
1216
|
+
at: min(300),
|
|
1217
|
+
},
|
|
1218
|
+
];
|
|
1219
|
+
const SEED_ANALYTICS = {
|
|
1220
|
+
range: '30d',
|
|
1221
|
+
kpiCards: [
|
|
1222
|
+
{ label: 'Calls handled', value: '1,247', delta: '+12%', tone: 'pos', sub: 'last 30 days' },
|
|
1223
|
+
{ label: 'AI resolution rate', value: '62%', delta: '+4%', tone: 'pos', sub: 'without transfer' },
|
|
1224
|
+
{ label: 'Avg handle time', value: '3:06', delta: '−8s', tone: 'pos', sub: 'vs prior period' },
|
|
1225
|
+
{ label: 'Transfers', value: '14%', delta: '−2%', tone: 'neutral', sub: 'patient requested human' },
|
|
1226
|
+
],
|
|
1227
|
+
volume: {
|
|
1228
|
+
timezone: 'America/Chicago',
|
|
1229
|
+
startDate: '2025-05-22',
|
|
1230
|
+
endDate: '2025-05-28',
|
|
1231
|
+
points: [
|
|
1232
|
+
{ date: '2025-05-22', day: 'Mon', label: 'Mon May 22', count: 42 },
|
|
1233
|
+
{ date: '2025-05-23', day: 'Tue', label: 'Tue May 23', count: 58 },
|
|
1234
|
+
{ date: '2025-05-24', day: 'Wed', label: 'Wed May 24', count: 51 },
|
|
1235
|
+
{ date: '2025-05-25', day: 'Thu', label: 'Thu May 25', count: 63 },
|
|
1236
|
+
{ date: '2025-05-26', day: 'Fri', label: 'Fri May 26', count: 47 },
|
|
1237
|
+
{ date: '2025-05-27', day: 'Sat', label: 'Sat May 27', count: 22 },
|
|
1238
|
+
{ date: '2025-05-28', day: 'Sun', label: 'Sun May 28', count: 18 },
|
|
1239
|
+
],
|
|
1240
|
+
},
|
|
1241
|
+
outcomes: [
|
|
1242
|
+
{ label: 'Completed by AI', value: 62, color: '#3681F4' },
|
|
1243
|
+
{ label: 'Transferred', value: 18, color: '#8E51FF' },
|
|
1244
|
+
{ label: 'Voicemail', value: 8, color: '#71717A' },
|
|
1245
|
+
{ label: 'Abandoned', value: 12, color: '#F03740' },
|
|
1246
|
+
],
|
|
1247
|
+
reasons: [
|
|
1248
|
+
{ label: 'Scheduling', value: 38 },
|
|
1249
|
+
{ label: 'Refills', value: 22 },
|
|
1250
|
+
{ label: 'Billing', value: 15 },
|
|
1251
|
+
{ label: 'Directions', value: 10 },
|
|
1252
|
+
{ label: 'Other', value: 15 },
|
|
1253
|
+
],
|
|
1254
|
+
transfers: [
|
|
1255
|
+
{ label: 'Billing', value: 34 },
|
|
1256
|
+
{ label: 'Clinical', value: 28 },
|
|
1257
|
+
{ label: 'Insurance', value: 18 },
|
|
1258
|
+
{ label: 'Emergency', value: 5 },
|
|
1259
|
+
],
|
|
1260
|
+
peakHours: {
|
|
1261
|
+
timezone: 'America/Chicago',
|
|
1262
|
+
points: Array.from({ length: 24 }, (_, hour) => ({
|
|
1263
|
+
hour,
|
|
1264
|
+
label: hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`,
|
|
1265
|
+
count: hour >= 8 && hour <= 17 ? 20 + Math.round(Math.sin((hour - 8) / 4) * 15) : 3 + (hour % 5),
|
|
1266
|
+
})),
|
|
1267
|
+
},
|
|
1268
|
+
kpis: {
|
|
1269
|
+
totalCalls: 1247,
|
|
1270
|
+
avgDuration: 186,
|
|
1271
|
+
bookingRate: 34,
|
|
1272
|
+
transferRate: 14,
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
const SEED_SETTINGS = {
|
|
1276
|
+
general: {
|
|
1277
|
+
practiceName: 'Riverside Family Medicine',
|
|
1278
|
+
timezone: 'America/Chicago',
|
|
1279
|
+
language: 'English',
|
|
1280
|
+
agentName: 'Edvak AI',
|
|
1281
|
+
welcomeMessage: 'Thank you for calling Riverside Family Medicine. This call may be recorded. How can I help you today?',
|
|
1282
|
+
ttsVoiceId: '7EzWGsX10sAS4c9m9cPf',
|
|
1283
|
+
},
|
|
1284
|
+
phone: { mainNumber: '+1 (512) 555-0100', afterHours: true, ringTimeout: 30 },
|
|
1285
|
+
tasks: { autoCreate: true, defaultAssignee: 'Front desk' },
|
|
1286
|
+
scheduling: { leadTimeHours: 4, allowSameDay: true },
|
|
1287
|
+
refill: { autoApproveDays: 7, requireStaffReview: true },
|
|
1288
|
+
verify: { requireDob: true, requireName: true, maxAttempts: 3 },
|
|
1289
|
+
escalation: { sentimentThreshold: 'frustrated', urgentKeywords: 'emergency, chest pain' },
|
|
1290
|
+
outbound: { enabled: true, callerId: '+1 (512) 555-0100' },
|
|
1291
|
+
safety: [
|
|
1292
|
+
{ id: 's1', label: 'HIPAA logging enabled', status: 'ok' },
|
|
1293
|
+
{ id: 's2', label: 'Recording consent script', status: 'ok' },
|
|
1294
|
+
{ id: 's3', label: 'Emergency routing tested', status: 'warn' },
|
|
1295
|
+
{ id: 's4', label: 'PHI redaction in transcripts', status: 'ok' },
|
|
1296
|
+
],
|
|
1297
|
+
transcripts: { retainDays: 90, redactPhi: true },
|
|
1298
|
+
advanced: { modelVersion: 'edvak-v2', debugMode: false },
|
|
1299
|
+
effectiveWelcomeMessage: 'Thank you for calling Riverside Family Medicine. This call may be recorded. How can I help you today?',
|
|
1300
|
+
};
|
|
1301
|
+
const SEED_SETUP = {
|
|
1302
|
+
readiness: 92,
|
|
1303
|
+
steps: [
|
|
1304
|
+
{ id: 1, title: 'Practice profile', description: 'Name, locations, hours', complete: true },
|
|
1305
|
+
{ id: 2, title: 'Phone routing', description: 'Connect Vonage numbers', complete: true },
|
|
1306
|
+
{ id: 3, title: 'EHR integration', description: 'FHIR scheduling & charts', complete: true },
|
|
1307
|
+
{ id: 4, title: 'Welcome message', description: 'Inbound call greeting', complete: true },
|
|
1308
|
+
{ id: 5, title: 'Verification rules', description: 'Identity checks', complete: true },
|
|
1309
|
+
{ id: 6, title: 'Task routing', description: 'Assignees & SLAs', complete: true },
|
|
1310
|
+
{ id: 7, title: 'Escalation paths', description: 'Human handoff rules', complete: true },
|
|
1311
|
+
{ id: 8, title: 'Safety checklist', description: 'Compliance review', complete: false },
|
|
1312
|
+
{ id: 9, title: 'Test call', description: 'Simulated inbound call', complete: false },
|
|
1313
|
+
{ id: 10, title: 'Staff training', description: 'Takeover & monitoring', complete: true },
|
|
1314
|
+
{ id: 11, title: 'Go live', description: 'Activate AI receptionist', complete: false },
|
|
1315
|
+
],
|
|
1316
|
+
testTranscript: [
|
|
1317
|
+
{ who: 'ai', text: 'Test call started — simulating inbound patient.' },
|
|
1318
|
+
{ who: 'patient', text: 'I would like to book an appointment.' },
|
|
1319
|
+
{ who: 'ai', text: 'Happy to help. May I verify your date of birth?' },
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
const SEED_LOCATIONS = [
|
|
1323
|
+
{ id: 'loc-1', name: 'Riverside Main' },
|
|
1324
|
+
{ id: 'loc-2', name: 'North Clinic' },
|
|
1325
|
+
{ id: 'loc-3', name: 'South Clinic' },
|
|
1326
|
+
];
|
|
1327
|
+
const SEED_PROVIDERS = [
|
|
1328
|
+
{ id: 'prov-1', name: 'Dr. Elena Chen' },
|
|
1329
|
+
{ id: 'prov-2', name: 'Dr. Raj Patel' },
|
|
1330
|
+
{ id: 'prov-3', name: 'Dr. Amy Foster' },
|
|
1331
|
+
];
|
|
1332
|
+
function buildDashboard() {
|
|
1333
|
+
return {
|
|
1334
|
+
stats: {
|
|
1335
|
+
activeCalls: SEED_LIVE_CALLS.length,
|
|
1336
|
+
answeredToday: 47,
|
|
1337
|
+
appointmentsBooked: 6,
|
|
1338
|
+
tasksCreated: 8,
|
|
1339
|
+
completionRate: 86,
|
|
1340
|
+
},
|
|
1341
|
+
liveCalls: SEED_LIVE_CALLS,
|
|
1342
|
+
latestTasks: SEED_TASKS.slice(0, 5),
|
|
1343
|
+
latestAppointments: [
|
|
1344
|
+
{
|
|
1345
|
+
id: 'appt-ai-1',
|
|
1346
|
+
appointmentId: 'appt-ai-1',
|
|
1347
|
+
patientName: 'Maria Garcia',
|
|
1348
|
+
providerName: 'Dr. Elena Chen',
|
|
1349
|
+
startTime: Date.now() + 86_400_000,
|
|
1350
|
+
createdAt: min(25),
|
|
1351
|
+
callId: 'HC-9001',
|
|
1352
|
+
status: 'booked',
|
|
1353
|
+
},
|
|
1354
|
+
],
|
|
1355
|
+
recentHistory: SEED_HISTORY.slice(0, 5),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function getStreamingLines() {
|
|
1359
|
+
return streamingLines;
|
|
1360
|
+
}
|
|
1361
|
+
function cloneLiveCalls() {
|
|
1362
|
+
return SEED_LIVE_CALLS.map((c) => ({
|
|
1363
|
+
...c,
|
|
1364
|
+
tools: c.tools.map((t) => ({ ...t })),
|
|
1365
|
+
transcript: c.transcript.map((l) => ({ ...l })),
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
function cloneHistory() {
|
|
1369
|
+
return SEED_HISTORY.map((h) => ({ ...h }));
|
|
1370
|
+
}
|
|
1371
|
+
function cloneTasks() {
|
|
1372
|
+
return SEED_TASKS.map((t) => ({ ...t }));
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const DEFAULT_PLACEHOLDERS = [
|
|
1376
|
+
{ token: '{{patientName}}', label: 'Patient name', sampleValue: 'Maria Garcia' },
|
|
1377
|
+
{ token: '{{appointmentDate}}', label: 'Appointment date', sampleValue: 'March 15' },
|
|
1378
|
+
{ token: '{{appointmentTime}}', label: 'Appointment time', sampleValue: '2:30 PM' },
|
|
1379
|
+
{ token: '{{providerName}}', label: 'Provider name', sampleValue: 'Dr. Patel' },
|
|
1380
|
+
{ token: '{{practiceName}}', label: 'Practice name', sampleValue: 'Edvak Family Medicine' },
|
|
1381
|
+
];
|
|
1382
|
+
const SEED_OUTBOUND_PURPOSES = [
|
|
1383
|
+
{
|
|
1384
|
+
purposeId: 'purpose-confirm-appt',
|
|
1385
|
+
label: 'Confirm appointment',
|
|
1386
|
+
description: 'Remind patient of upcoming visit and confirm attendance.',
|
|
1387
|
+
sampleOpening: 'Hi {{patientName}}, this is {{practiceName}} calling to confirm your appointment on {{appointmentDate}} at {{appointmentTime}} with {{providerName}}.',
|
|
1388
|
+
openingTemplate: 'Hi {{patientName}}, this is {{practiceName}} calling to confirm your appointment on {{appointmentDate}} at {{appointmentTime}} with {{providerName}}.',
|
|
1389
|
+
agentContextHint: 'Confirm attendance; offer reschedule if patient cannot make it.',
|
|
1390
|
+
requiredFields: ['appointmentDate', 'appointmentTime', 'providerName'],
|
|
1391
|
+
allowsCustomScript: false,
|
|
1392
|
+
isSystem: true,
|
|
1393
|
+
isActive: true,
|
|
1394
|
+
sortOrder: 1,
|
|
1395
|
+
placeholders: DEFAULT_PLACEHOLDERS,
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
purposeId: 'purpose-recall',
|
|
1399
|
+
label: 'Recall / follow-up',
|
|
1400
|
+
description: 'Invite patient to schedule a follow-up visit.',
|
|
1401
|
+
sampleOpening: 'Hello {{patientName}}, {{practiceName}} here. We would like to help you schedule a follow-up with {{providerName}}.',
|
|
1402
|
+
openingTemplate: 'Hello {{patientName}}, {{practiceName}} here. We would like to help you schedule a follow-up with {{providerName}}.',
|
|
1403
|
+
agentContextHint: null,
|
|
1404
|
+
requiredFields: ['providerName'],
|
|
1405
|
+
allowsCustomScript: true,
|
|
1406
|
+
isSystem: true,
|
|
1407
|
+
isActive: true,
|
|
1408
|
+
sortOrder: 2,
|
|
1409
|
+
placeholders: DEFAULT_PLACEHOLDERS,
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
purposeId: 'purpose-custom',
|
|
1413
|
+
label: 'Custom outreach',
|
|
1414
|
+
description: 'Staff-defined script for one-off calls.',
|
|
1415
|
+
sampleOpening: '',
|
|
1416
|
+
openingTemplate: 'Hi {{patientName}}, this is {{practiceName}}.',
|
|
1417
|
+
agentContextHint: 'Follow the custom script closely.',
|
|
1418
|
+
requiredFields: ['customScript'],
|
|
1419
|
+
allowsCustomScript: true,
|
|
1420
|
+
isSystem: false,
|
|
1421
|
+
isActive: false,
|
|
1422
|
+
sortOrder: 99,
|
|
1423
|
+
placeholders: DEFAULT_PLACEHOLDERS,
|
|
1424
|
+
},
|
|
1425
|
+
];
|
|
1426
|
+
|
|
1427
|
+
const MOCK_DELAY_MS = 200;
|
|
1428
|
+
class AiReceptionistApiMockService {
|
|
1429
|
+
constructor() {
|
|
1430
|
+
this.liveCalls = signal(cloneLiveCalls());
|
|
1431
|
+
this.tasks = signal(cloneTasks());
|
|
1432
|
+
this.history = signal(cloneHistory());
|
|
1433
|
+
this.settings = signal(structuredClone(SEED_SETTINGS));
|
|
1434
|
+
this.status = signal({
|
|
1435
|
+
agentOn: true,
|
|
1436
|
+
mode: 'after-hours',
|
|
1437
|
+
readiness: SEED_SETUP.readiness,
|
|
1438
|
+
});
|
|
1439
|
+
this.outboundPurposes = signal(structuredClone(SEED_OUTBOUND_PURPOSES));
|
|
1440
|
+
}
|
|
1441
|
+
mock(data) {
|
|
1442
|
+
return of({ data }).pipe(delay(MOCK_DELAY_MS));
|
|
1443
|
+
}
|
|
1444
|
+
getStatus() {
|
|
1445
|
+
return this.mock(this.status());
|
|
1446
|
+
}
|
|
1447
|
+
setAgentOn(on) {
|
|
1448
|
+
this.status.update((s) => ({ ...s, agentOn: on }));
|
|
1449
|
+
}
|
|
1450
|
+
getDashboard() {
|
|
1451
|
+
const dash = buildDashboard();
|
|
1452
|
+
dash.liveCalls = [];
|
|
1453
|
+
return this.mock(dash);
|
|
1454
|
+
}
|
|
1455
|
+
getLiveCalls() {
|
|
1456
|
+
return this.mock(this.liveCalls());
|
|
1457
|
+
}
|
|
1458
|
+
getLiveCall(id) {
|
|
1459
|
+
const call = this.liveCalls().find((c) => c.id === id) ?? null;
|
|
1460
|
+
return this.mock(call ? structuredClone(call) : null);
|
|
1461
|
+
}
|
|
1462
|
+
getCallTranscript(id) {
|
|
1463
|
+
const call = this.liveCalls().find((c) => c.id === id);
|
|
1464
|
+
return this.mock(call ? call.transcript.map((l) => ({ ...l })) : []);
|
|
1465
|
+
}
|
|
1466
|
+
getCallSummary(id) {
|
|
1467
|
+
const detail = SEED_HISTORY_DETAILS[id];
|
|
1468
|
+
if (detail) {
|
|
1469
|
+
return this.mock({
|
|
1470
|
+
summary: detail.summary,
|
|
1471
|
+
transcript: detail.transcript.map((l) => ({ ...l })),
|
|
1472
|
+
tools: detail.tools.map((t) => ({ ...t })),
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
const call = this.liveCalls().find((c) => c.id === id);
|
|
1476
|
+
if (call) {
|
|
1477
|
+
return this.mock({
|
|
1478
|
+
summary: 'Call summary will appear here after processing.',
|
|
1479
|
+
transcript: call.transcript.map((l) => ({ ...l })),
|
|
1480
|
+
tools: call.tools.map((t) => ({ ...t })),
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
return this.mock(null);
|
|
1484
|
+
}
|
|
1485
|
+
getCallRecording(callId) {
|
|
1486
|
+
const detail = SEED_HISTORY_DETAILS[callId];
|
|
1487
|
+
if (detail?.recordingUrl) {
|
|
1488
|
+
const meta = {
|
|
1489
|
+
callId,
|
|
1490
|
+
status: 'ready',
|
|
1491
|
+
url: `/api/ai-receptionist/calls/${callId}/recording/audio`,
|
|
1492
|
+
contentType: 'audio/ogg',
|
|
1493
|
+
durationSeconds: detail.duration || 120,
|
|
1494
|
+
};
|
|
1495
|
+
return of(meta).pipe(delay(MOCK_DELAY_MS));
|
|
1496
|
+
}
|
|
1497
|
+
const unavailable = {
|
|
1498
|
+
callId,
|
|
1499
|
+
status: 'unavailable',
|
|
1500
|
+
url: null,
|
|
1501
|
+
message: 'No recording available for this call',
|
|
1502
|
+
};
|
|
1503
|
+
return of(unavailable).pipe(delay(MOCK_DELAY_MS));
|
|
1504
|
+
}
|
|
1505
|
+
getCallRecordingBlob() {
|
|
1506
|
+
return of(new Blob([], { type: 'audio/ogg' })).pipe(delay(MOCK_DELAY_MS));
|
|
1507
|
+
}
|
|
1508
|
+
getHistory(filters) {
|
|
1509
|
+
let rows = this.history();
|
|
1510
|
+
const q = filters?.q?.toLowerCase().trim();
|
|
1511
|
+
if (q) {
|
|
1512
|
+
rows = rows.filter((h) => h.name.toLowerCase().includes(q) ||
|
|
1513
|
+
h.phone.includes(q) ||
|
|
1514
|
+
h.reason.toLowerCase().includes(q) ||
|
|
1515
|
+
h.id.toLowerCase().includes(q));
|
|
1516
|
+
}
|
|
1517
|
+
if (filters?.dir)
|
|
1518
|
+
rows = rows.filter((h) => h.direction === filters.dir);
|
|
1519
|
+
if (filters?.outcome) {
|
|
1520
|
+
rows = rows.filter((h) => h.outcome.toLowerCase().includes(filters.outcome.toLowerCase()));
|
|
1521
|
+
}
|
|
1522
|
+
if (filters?.location)
|
|
1523
|
+
rows = rows.filter((h) => h.location === filters.location);
|
|
1524
|
+
if (filters?.review)
|
|
1525
|
+
rows = rows.filter((h) => h.review === filters.review);
|
|
1526
|
+
return this.mock(rows);
|
|
1527
|
+
}
|
|
1528
|
+
getCallDetail(id) {
|
|
1529
|
+
const detail = SEED_HISTORY_DETAILS[id] ?? null;
|
|
1530
|
+
return this.mock(detail ? structuredClone(detail) : null);
|
|
1531
|
+
}
|
|
1532
|
+
regenerateCallSummary(callId) {
|
|
1533
|
+
const detail = SEED_HISTORY_DETAILS[callId];
|
|
1534
|
+
if (!detail) {
|
|
1535
|
+
return throwError(() => ({ message: 'Call not found.', status: 404 }));
|
|
1536
|
+
}
|
|
1537
|
+
if (!detail.transcript.length) {
|
|
1538
|
+
return throwError(() => ({ message: 'No transcript on this call.', status: 400 }));
|
|
1539
|
+
}
|
|
1540
|
+
const turnCount = detail.transcript.filter((line) => line.who !== 'system').length;
|
|
1541
|
+
const summary = `Regenerated summary (${new Date().toLocaleString()}): ${detail.summary}`;
|
|
1542
|
+
detail.summary = summary;
|
|
1543
|
+
SEED_HISTORY_DETAILS[callId] = detail;
|
|
1544
|
+
return this.mock({
|
|
1545
|
+
callId,
|
|
1546
|
+
summary,
|
|
1547
|
+
summaryModel: 'gpt-4o-mini',
|
|
1548
|
+
summaryGeneratedAt: new Date().toISOString(),
|
|
1549
|
+
transcriptTurnCount: turnCount,
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
postTakeover(id) {
|
|
1553
|
+
this.patchLive(id, (c) => ({ ...c, status: 'takeover', assigned: 'You' }));
|
|
1554
|
+
return this.mock({ success: true });
|
|
1555
|
+
}
|
|
1556
|
+
postResumeAi(id) {
|
|
1557
|
+
this.patchLive(id, (c) => ({ ...c, status: 'ai', assigned: null, aiPaused: false }));
|
|
1558
|
+
return this.mock({ success: true });
|
|
1559
|
+
}
|
|
1560
|
+
postPause(id) {
|
|
1561
|
+
this.patchLive(id, (c) => ({ ...c, aiPaused: true }));
|
|
1562
|
+
return this.mock({ success: true });
|
|
1563
|
+
}
|
|
1564
|
+
postTransfer(id) {
|
|
1565
|
+
this.patchLive(id, (c) => ({ ...c, status: 'transferring' }));
|
|
1566
|
+
return this.mock({ success: true });
|
|
1567
|
+
}
|
|
1568
|
+
postEnd(id) {
|
|
1569
|
+
this.liveCalls.update((calls) => calls.filter((c) => c.id !== id));
|
|
1570
|
+
return this.mock({ success: true });
|
|
1571
|
+
}
|
|
1572
|
+
postMessage(id, text) {
|
|
1573
|
+
this.patchLive(id, (c) => ({
|
|
1574
|
+
...c,
|
|
1575
|
+
transcript: [...c.transcript, { who: 'staff', text, at: Date.now() }],
|
|
1576
|
+
}));
|
|
1577
|
+
return this.mock({ success: true });
|
|
1578
|
+
}
|
|
1579
|
+
postReview(id) {
|
|
1580
|
+
this.history.update((rows) => rows.map((h) => (h.id === id ? { ...h, review: 'reviewed' } : h)));
|
|
1581
|
+
const detail = SEED_HISTORY_DETAILS[id];
|
|
1582
|
+
if (detail) {
|
|
1583
|
+
detail.review = 'reviewed';
|
|
1584
|
+
}
|
|
1585
|
+
return this.mock({ review: 'reviewed' });
|
|
1586
|
+
}
|
|
1587
|
+
getTasks(filters) {
|
|
1588
|
+
let rows = this.tasks();
|
|
1589
|
+
if (filters?.status)
|
|
1590
|
+
rows = rows.filter((t) => t.status === filters.status);
|
|
1591
|
+
if (filters?.type)
|
|
1592
|
+
rows = rows.filter((t) => t.type === filters.type);
|
|
1593
|
+
if (filters?.source)
|
|
1594
|
+
rows = rows.filter((t) => t.source === filters.source);
|
|
1595
|
+
if (filters?.callId)
|
|
1596
|
+
rows = rows.filter((t) => t.callId === filters.callId);
|
|
1597
|
+
return this.mock(rows);
|
|
1598
|
+
}
|
|
1599
|
+
getCallTasks(callId) {
|
|
1600
|
+
return this.getTasks({ callId });
|
|
1601
|
+
}
|
|
1602
|
+
createTask(req) {
|
|
1603
|
+
let description = req.description?.trim() ?? '';
|
|
1604
|
+
if (!description && req.callId) {
|
|
1605
|
+
description = SEED_HISTORY_DETAILS[req.callId]?.summary ?? '';
|
|
1606
|
+
}
|
|
1607
|
+
const task = {
|
|
1608
|
+
id: `TK-${Date.now()}`,
|
|
1609
|
+
created: Date.now(),
|
|
1610
|
+
title: req.title.trim(),
|
|
1611
|
+
type: req.type,
|
|
1612
|
+
description,
|
|
1613
|
+
patient: req.patient?.trim() || '—',
|
|
1614
|
+
priority: req.priority ?? 'normal',
|
|
1615
|
+
status: 'open',
|
|
1616
|
+
source: 'manual',
|
|
1617
|
+
assignee: req.assignee?.trim() || 'Front desk',
|
|
1618
|
+
callId: req.callId,
|
|
1619
|
+
};
|
|
1620
|
+
this.tasks.update((rows) => [task, ...rows]);
|
|
1621
|
+
return this.mock(task);
|
|
1622
|
+
}
|
|
1623
|
+
patchTask(id, status) {
|
|
1624
|
+
let updated = null;
|
|
1625
|
+
this.tasks.update((rows) => rows.map((t) => {
|
|
1626
|
+
if (t.id !== id)
|
|
1627
|
+
return t;
|
|
1628
|
+
updated = { ...t, status };
|
|
1629
|
+
return updated;
|
|
1630
|
+
}));
|
|
1631
|
+
return this.mock(updated);
|
|
1632
|
+
}
|
|
1633
|
+
getAnalytics(range) {
|
|
1634
|
+
return this.mock({ ...SEED_ANALYTICS, range });
|
|
1635
|
+
}
|
|
1636
|
+
getSettings() {
|
|
1637
|
+
const data = this.withEffectiveWelcome(this.settings());
|
|
1638
|
+
return this.mock(data);
|
|
1639
|
+
}
|
|
1640
|
+
putSettings(next) {
|
|
1641
|
+
const saved = this.withEffectiveWelcome(structuredClone(next));
|
|
1642
|
+
this.settings.set(saved);
|
|
1643
|
+
return this.mock(saved);
|
|
1644
|
+
}
|
|
1645
|
+
previewVoice(_voiceId) {
|
|
1646
|
+
// Silent tiny wav so mock mode does not break the Play button.
|
|
1647
|
+
const silent = new Uint8Array([
|
|
1648
|
+
0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74,
|
|
1649
|
+
0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x40, 0x1f, 0x00, 0x00, 0x80, 0x3e,
|
|
1650
|
+
0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00,
|
|
1651
|
+
]);
|
|
1652
|
+
return of(new Blob([silent], { type: 'audio/wav' })).pipe(delay(MOCK_DELAY_MS));
|
|
1653
|
+
}
|
|
1654
|
+
withEffectiveWelcome(settings) {
|
|
1655
|
+
return {
|
|
1656
|
+
...settings,
|
|
1657
|
+
effectiveWelcomeMessage: computeEffectiveWelcomeMessage(settings),
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
getAudit(q, actor) {
|
|
1661
|
+
let rows = SEED_AUDIT;
|
|
1662
|
+
if (q) {
|
|
1663
|
+
const needle = q.toLowerCase();
|
|
1664
|
+
rows = rows.filter((e) => e.action.toLowerCase().includes(needle) ||
|
|
1665
|
+
e.detail.toLowerCase().includes(needle) ||
|
|
1666
|
+
e.target.toLowerCase().includes(needle));
|
|
1667
|
+
}
|
|
1668
|
+
if (actor)
|
|
1669
|
+
rows = rows.filter((e) => e.actor.toLowerCase().includes(actor.toLowerCase()));
|
|
1670
|
+
return this.mock(rows);
|
|
1671
|
+
}
|
|
1672
|
+
getSetup() {
|
|
1673
|
+
return this.mock(SEED_SETUP);
|
|
1674
|
+
}
|
|
1675
|
+
activateSetup() {
|
|
1676
|
+
return this.mock({ success: true });
|
|
1677
|
+
}
|
|
1678
|
+
getLocations() {
|
|
1679
|
+
return this.mock(SEED_LOCATIONS);
|
|
1680
|
+
}
|
|
1681
|
+
getProviders() {
|
|
1682
|
+
return this.mock(SEED_PROVIDERS);
|
|
1683
|
+
}
|
|
1684
|
+
getStreamLines() {
|
|
1685
|
+
return getStreamingLines();
|
|
1686
|
+
}
|
|
1687
|
+
appendStreamLine(callId, line) {
|
|
1688
|
+
this.patchLive(callId, (c) => ({
|
|
1689
|
+
...c,
|
|
1690
|
+
transcript: [...c.transcript, line],
|
|
1691
|
+
}));
|
|
1692
|
+
}
|
|
1693
|
+
usesMockStreaming() {
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
getCallApiLogs(callId) {
|
|
1697
|
+
const template = SEED_CALL_API_LOGS[callId] ?? SEED_CALL_API_LOGS['LC-4821'] ?? [];
|
|
1698
|
+
const items = template.map((row) => ({ ...row, callId }));
|
|
1699
|
+
return of({ items: structuredClone(items), total: items.length }).pipe(delay(MOCK_DELAY_MS));
|
|
1700
|
+
}
|
|
1701
|
+
getApiLogs(filters = {}) {
|
|
1702
|
+
let rows = allSeedLogs();
|
|
1703
|
+
if (filters.callId) {
|
|
1704
|
+
rows = rows.filter((r) => r.callId === filters.callId);
|
|
1705
|
+
}
|
|
1706
|
+
if (filters.provider) {
|
|
1707
|
+
const providers = filters.provider.split(',').filter(Boolean);
|
|
1708
|
+
rows = rows.filter((r) => providers.includes(r.provider));
|
|
1709
|
+
}
|
|
1710
|
+
if (filters.success !== undefined) {
|
|
1711
|
+
rows = rows.filter((r) => r.success === filters.success);
|
|
1712
|
+
}
|
|
1713
|
+
if (filters.statusCode != null) {
|
|
1714
|
+
rows = rows.filter((r) => r.statusCode === filters.statusCode);
|
|
1715
|
+
}
|
|
1716
|
+
if (filters.operation) {
|
|
1717
|
+
const op = filters.operation.toLowerCase();
|
|
1718
|
+
rows = rows.filter((r) => r.operation.toLowerCase().includes(op));
|
|
1719
|
+
}
|
|
1720
|
+
if (filters.search) {
|
|
1721
|
+
const q = filters.search.toLowerCase();
|
|
1722
|
+
rows = rows.filter((r) => r.operation.toLowerCase().includes(q) ||
|
|
1723
|
+
r.urlPath.toLowerCase().includes(q) ||
|
|
1724
|
+
(r.errorMessage?.toLowerCase().includes(q) ?? false) ||
|
|
1725
|
+
(r.toolName?.toLowerCase().includes(q) ?? false));
|
|
1726
|
+
}
|
|
1727
|
+
if (filters.from) {
|
|
1728
|
+
const from = Date.parse(filters.from);
|
|
1729
|
+
rows = rows.filter((r) => Date.parse(r.startedAt) >= from);
|
|
1730
|
+
}
|
|
1731
|
+
if (filters.to) {
|
|
1732
|
+
const to = Date.parse(filters.to);
|
|
1733
|
+
rows = rows.filter((r) => Date.parse(r.startedAt) <= to);
|
|
1734
|
+
}
|
|
1735
|
+
rows = [...rows].sort((a, b) => Date.parse(a.startedAt) - Date.parse(b.startedAt));
|
|
1736
|
+
const page = filters.page ?? 1;
|
|
1737
|
+
const limit = filters.limit ?? 50;
|
|
1738
|
+
const start = (page - 1) * limit;
|
|
1739
|
+
const items = rows.slice(start, start + limit);
|
|
1740
|
+
return of({ items, total: rows.length }).pipe(delay(MOCK_DELAY_MS));
|
|
1741
|
+
}
|
|
1742
|
+
getApiLogDetail(logId) {
|
|
1743
|
+
const detail = SEED_API_LOG_DETAILS[logId];
|
|
1744
|
+
if (detail) {
|
|
1745
|
+
return this.mock(structuredClone(detail));
|
|
1746
|
+
}
|
|
1747
|
+
const listItem = allSeedLogs().find((r) => r.id === logId);
|
|
1748
|
+
if (!listItem) {
|
|
1749
|
+
return of({ data: null }).pipe(delay(MOCK_DELAY_MS));
|
|
1750
|
+
}
|
|
1751
|
+
const fallback = {
|
|
1752
|
+
...listItem,
|
|
1753
|
+
traceId: null,
|
|
1754
|
+
requestBody: listItem.requestPreview ? { preview: listItem.requestPreview } : null,
|
|
1755
|
+
responseBody: listItem.responsePreview ? { preview: listItem.responsePreview } : null,
|
|
1756
|
+
metadata: null,
|
|
1757
|
+
completedAt: listItem.startedAt,
|
|
1758
|
+
};
|
|
1759
|
+
return this.mock(fallback);
|
|
1760
|
+
}
|
|
1761
|
+
getApiLogStats() {
|
|
1762
|
+
return this.mock(structuredClone(SEED_API_LOG_STATS));
|
|
1763
|
+
}
|
|
1764
|
+
getOutboundPurposes(includeInactive = false) {
|
|
1765
|
+
let rows = [...this.outboundPurposes()].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1766
|
+
if (!includeInactive) {
|
|
1767
|
+
rows = rows.filter((p) => p.isActive);
|
|
1768
|
+
}
|
|
1769
|
+
return of({ purposes: structuredClone(rows) }).pipe(delay(MOCK_DELAY_MS));
|
|
1770
|
+
}
|
|
1771
|
+
createOutboundPurpose(body) {
|
|
1772
|
+
const purpose = {
|
|
1773
|
+
purposeId: `purpose-${Date.now()}`,
|
|
1774
|
+
label: body.label,
|
|
1775
|
+
description: body.description ?? '',
|
|
1776
|
+
sampleOpening: body.sampleOpening ?? '',
|
|
1777
|
+
openingTemplate: body.openingTemplate,
|
|
1778
|
+
agentContextHint: body.agentContextHint ?? null,
|
|
1779
|
+
requiredFields: body.requiredFields,
|
|
1780
|
+
allowsCustomScript: body.allowsCustomScript ?? false,
|
|
1781
|
+
isSystem: false,
|
|
1782
|
+
isActive: true,
|
|
1783
|
+
sortOrder: body.sortOrder ?? this.outboundPurposes().length + 1,
|
|
1784
|
+
placeholders: structuredClone(DEFAULT_PLACEHOLDERS),
|
|
1785
|
+
};
|
|
1786
|
+
this.outboundPurposes.update((list) => [...list, purpose]);
|
|
1787
|
+
return of(structuredClone(purpose)).pipe(delay(MOCK_DELAY_MS));
|
|
1788
|
+
}
|
|
1789
|
+
updateOutboundPurpose(purposeId, body) {
|
|
1790
|
+
let updated = null;
|
|
1791
|
+
this.outboundPurposes.update((list) => list.map((p) => {
|
|
1792
|
+
if (p.purposeId !== purposeId)
|
|
1793
|
+
return p;
|
|
1794
|
+
updated = {
|
|
1795
|
+
...p,
|
|
1796
|
+
...body,
|
|
1797
|
+
label: body.label ?? p.label,
|
|
1798
|
+
description: body.description ?? p.description,
|
|
1799
|
+
sampleOpening: body.sampleOpening ?? p.sampleOpening,
|
|
1800
|
+
openingTemplate: body.openingTemplate ?? p.openingTemplate,
|
|
1801
|
+
agentContextHint: body.agentContextHint !== undefined ? body.agentContextHint : p.agentContextHint,
|
|
1802
|
+
requiredFields: body.requiredFields ?? p.requiredFields,
|
|
1803
|
+
allowsCustomScript: body.allowsCustomScript ?? p.allowsCustomScript,
|
|
1804
|
+
isActive: body.isActive ?? p.isActive,
|
|
1805
|
+
sortOrder: body.sortOrder ?? p.sortOrder,
|
|
1806
|
+
};
|
|
1807
|
+
return updated;
|
|
1808
|
+
}));
|
|
1809
|
+
if (!updated) {
|
|
1810
|
+
return throwError(() => new Error('Purpose not found'));
|
|
1811
|
+
}
|
|
1812
|
+
const saved = updated;
|
|
1813
|
+
return of(structuredClone(saved)).pipe(delay(MOCK_DELAY_MS));
|
|
1814
|
+
}
|
|
1815
|
+
deleteOutboundPurpose(purposeId) {
|
|
1816
|
+
const target = this.outboundPurposes().find((p) => p.purposeId === purposeId);
|
|
1817
|
+
if (!target) {
|
|
1818
|
+
return of({ success: false }).pipe(delay(MOCK_DELAY_MS));
|
|
1819
|
+
}
|
|
1820
|
+
if (target.isSystem) {
|
|
1821
|
+
return of({ success: false }).pipe(delay(MOCK_DELAY_MS));
|
|
1822
|
+
}
|
|
1823
|
+
this.outboundPurposes.update((list) => list.filter((p) => p.purposeId !== purposeId));
|
|
1824
|
+
return of({ success: true }).pipe(delay(MOCK_DELAY_MS));
|
|
1825
|
+
}
|
|
1826
|
+
placeOutboundCall(body) {
|
|
1827
|
+
return of({
|
|
1828
|
+
success: true,
|
|
1829
|
+
callId: `LC-${Math.floor(4800 + Math.random() * 100)}`,
|
|
1830
|
+
message: `Outbound call started to ${body.phone}`,
|
|
1831
|
+
}).pipe(delay(MOCK_DELAY_MS));
|
|
1832
|
+
}
|
|
1833
|
+
patchLive(id, fn) {
|
|
1834
|
+
this.liveCalls.update((calls) => calls.map((c) => (c.id === id ? fn(structuredClone(c)) : c)));
|
|
1835
|
+
}
|
|
1836
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiMockService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1837
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiMockService, providedIn: 'root' }); }
|
|
1838
|
+
}
|
|
1839
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiMockService, decorators: [{
|
|
1840
|
+
type: Injectable,
|
|
1841
|
+
args: [{ providedIn: 'root' }]
|
|
1842
|
+
}] });
|
|
1843
|
+
|
|
1844
|
+
class AiReceptionistApiService {
|
|
1845
|
+
constructor() {
|
|
1846
|
+
this.http = inject(HttpClient);
|
|
1847
|
+
this.mock = inject(AiReceptionistApiMockService);
|
|
1848
|
+
this.config = inject(VOICE_RECEPTIONIST_CONFIG);
|
|
1849
|
+
this.paths = createVoiceReceptionistPaths(this.config);
|
|
1850
|
+
this.arBase = this.paths.aiReceptionistPath();
|
|
1851
|
+
}
|
|
1852
|
+
get useMock() {
|
|
1853
|
+
return !!this.config.useMockApi;
|
|
1854
|
+
}
|
|
1855
|
+
getStatus() {
|
|
1856
|
+
if (this.useMock)
|
|
1857
|
+
return this.mock.getStatus();
|
|
1858
|
+
return this.http.get(`${this.arBase}/status`).pipe(map((body) => ({ data: mapStatusDto(body) })), catchError((err) => throwError(() => ({
|
|
1859
|
+
message: this.apiErrorMessage(err, 'Failed to load agent status.'),
|
|
1860
|
+
status: err.status,
|
|
1861
|
+
}))));
|
|
1862
|
+
}
|
|
1863
|
+
/** Prefer this for the dashboard On/Off switch — PUT { enabled }. */
|
|
1864
|
+
setAgentOnline(enabled) {
|
|
1865
|
+
if (this.useMock) {
|
|
1866
|
+
this.mock.setAgentOn(enabled);
|
|
1867
|
+
return this.mock.getStatus();
|
|
1868
|
+
}
|
|
1869
|
+
return this.http
|
|
1870
|
+
.put(`${this.arBase}/agent-online`, { enabled })
|
|
1871
|
+
.pipe(map((body) => ({ data: mapStatusDto(body) })), catchError((err) => throwError(() => ({
|
|
1872
|
+
message: this.apiErrorMessage(err, 'Failed to update agent online status.'),
|
|
1873
|
+
status: err.status,
|
|
1874
|
+
}))));
|
|
1875
|
+
}
|
|
1876
|
+
/** @deprecated Prefer setAgentOnline — kept for setup wizard activate flow. */
|
|
1877
|
+
setAgentOn(on) {
|
|
1878
|
+
if (this.useMock) {
|
|
1879
|
+
this.mock.setAgentOn(on);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const path = on ? 'activate' : 'deactivate';
|
|
1883
|
+
this.http.post(`${this.arBase}/${path}`, {}).subscribe();
|
|
1884
|
+
}
|
|
1885
|
+
getDashboard() {
|
|
1886
|
+
if (this.useMock)
|
|
1887
|
+
return this.mock.getDashboard();
|
|
1888
|
+
return forkJoin({
|
|
1889
|
+
summary: this.http.get(`${this.arBase}/dashboard/summary`),
|
|
1890
|
+
history: this.http.get(`${this.arBase}/calls/history`, {
|
|
1891
|
+
params: new HttpParams().set('page', '1').set('limit', '5'),
|
|
1892
|
+
}),
|
|
1893
|
+
tasks: this.http
|
|
1894
|
+
.get(`${this.arBase}/tasks`, {
|
|
1895
|
+
params: new HttpParams().set('source', 'voice').set('limit', '5'),
|
|
1896
|
+
})
|
|
1897
|
+
.pipe(catchError(() => of([]))),
|
|
1898
|
+
appointments: this.http
|
|
1899
|
+
.get(`${this.arBase}/dashboard/appointments`, { params: new HttpParams().set('limit', '5') })
|
|
1900
|
+
.pipe(catchError(() => of([]))),
|
|
1901
|
+
}).pipe(map(({ summary, history, tasks, appointments }) => ({
|
|
1902
|
+
data: {
|
|
1903
|
+
stats: mapDashboardSummaryDto(summary),
|
|
1904
|
+
liveCalls: [],
|
|
1905
|
+
latestTasks: asTaskList(tasks)
|
|
1906
|
+
.map(mapTaskDto)
|
|
1907
|
+
.sort((a, b) => b.created - a.created)
|
|
1908
|
+
.slice(0, 5),
|
|
1909
|
+
latestAppointments: (Array.isArray(appointments)
|
|
1910
|
+
? appointments
|
|
1911
|
+
: appointments.items ?? []).map(mapAiCreatedAppointmentDto),
|
|
1912
|
+
recentHistory: asHistoryList(history).map(mapHistoryCallDto),
|
|
1913
|
+
},
|
|
1914
|
+
})));
|
|
1915
|
+
}
|
|
1916
|
+
getLiveCalls() {
|
|
1917
|
+
if (this.useMock)
|
|
1918
|
+
return this.mock.getLiveCalls();
|
|
1919
|
+
return this.http.get(`${this.arBase}/calls/live`).pipe(map((body) => ({ data: asLiveCallList(body).map(mapLiveCallDto) })));
|
|
1920
|
+
}
|
|
1921
|
+
getLiveCall(id) {
|
|
1922
|
+
if (this.useMock)
|
|
1923
|
+
return this.mock.getLiveCall(id);
|
|
1924
|
+
const enc = encodeURIComponent(id);
|
|
1925
|
+
return this.http.get(`${this.arBase}/calls/${enc}`).pipe(map((dto) => ({ data: dto?.id ? mapLiveCallDto(dto) : null })), catchError(() => of({ data: null })));
|
|
1926
|
+
}
|
|
1927
|
+
getCallTranscript(id) {
|
|
1928
|
+
if (this.useMock)
|
|
1929
|
+
return this.mock.getCallTranscript(id);
|
|
1930
|
+
const enc = encodeURIComponent(id);
|
|
1931
|
+
return this.http.get(`${this.arBase}/calls/${enc}/transcript`).pipe(map((body) => ({ data: asTranscriptList(body).map(mapTranscriptLine) })), catchError(() => of({ data: [] })));
|
|
1932
|
+
}
|
|
1933
|
+
getCallSummary(id) {
|
|
1934
|
+
if (this.useMock)
|
|
1935
|
+
return this.mock.getCallSummary(id);
|
|
1936
|
+
const enc = encodeURIComponent(id);
|
|
1937
|
+
return this.http.get(`${this.arBase}/calls/${enc}/summary`).pipe(map((dto) => ({
|
|
1938
|
+
data: dto
|
|
1939
|
+
? {
|
|
1940
|
+
summary: dto.summary ?? '',
|
|
1941
|
+
transcript: (dto.transcript ?? []).map(mapTranscriptLine),
|
|
1942
|
+
tools: (dto.tools ?? []).map(mapToolEvent),
|
|
1943
|
+
}
|
|
1944
|
+
: null,
|
|
1945
|
+
})), catchError(() => of({ data: null })));
|
|
1946
|
+
}
|
|
1947
|
+
getCallRecording(callId) {
|
|
1948
|
+
if (this.useMock)
|
|
1949
|
+
return this.mock.getCallRecording(callId);
|
|
1950
|
+
const enc = encodeURIComponent(callId);
|
|
1951
|
+
return this.http.get(`${this.arBase}/calls/${enc}/recording`).pipe(map((dto) => mapRecordingMeta(dto)), catchError(() => of({
|
|
1952
|
+
callId,
|
|
1953
|
+
status: 'unavailable',
|
|
1954
|
+
url: null,
|
|
1955
|
+
message: 'No recording available for this call',
|
|
1956
|
+
})));
|
|
1957
|
+
}
|
|
1958
|
+
/** Fetch audio bytes with JWT (interceptor). Pass relative path from metadata.url. */
|
|
1959
|
+
getCallRecordingBlob(audioPath) {
|
|
1960
|
+
if (this.useMock)
|
|
1961
|
+
return this.mock.getCallRecordingBlob();
|
|
1962
|
+
const url = audioPath.startsWith('http') ? audioPath : `${this.paths.apiRoot()}${audioPath}`;
|
|
1963
|
+
return this.http.get(url, { responseType: 'blob' });
|
|
1964
|
+
}
|
|
1965
|
+
getHistory(filters) {
|
|
1966
|
+
if (this.useMock)
|
|
1967
|
+
return this.mock.getHistory(filters);
|
|
1968
|
+
let params = new HttpParams().set('page', '1').set('limit', '100');
|
|
1969
|
+
if (filters?.q)
|
|
1970
|
+
params = params.set('search', filters.q);
|
|
1971
|
+
if (filters?.dir)
|
|
1972
|
+
params = params.set('direction', filters.dir);
|
|
1973
|
+
if (filters?.outcome)
|
|
1974
|
+
params = params.set('outcome', filters.outcome);
|
|
1975
|
+
if (filters?.location)
|
|
1976
|
+
params = params.set('location', filters.location);
|
|
1977
|
+
if (filters?.review)
|
|
1978
|
+
params = params.set('review', filters.review);
|
|
1979
|
+
return this.http.get(`${this.arBase}/calls/history`, { params }).pipe(map((body) => ({ data: asHistoryList(body).map(mapHistoryCallDto) })));
|
|
1980
|
+
}
|
|
1981
|
+
getCallDetail(id) {
|
|
1982
|
+
if (this.useMock)
|
|
1983
|
+
return this.mock.getCallDetail(id);
|
|
1984
|
+
const enc = encodeURIComponent(id);
|
|
1985
|
+
return forkJoin({
|
|
1986
|
+
call: this.http.get(`${this.arBase}/calls/${enc}`),
|
|
1987
|
+
summary: this.http.get(`${this.arBase}/calls/${enc}/summary`),
|
|
1988
|
+
recording: this.http
|
|
1989
|
+
.get(`${this.arBase}/calls/${enc}/recording`)
|
|
1990
|
+
.pipe(catchError(() => of({ callId: id, url: null, message: '' }))),
|
|
1991
|
+
}).pipe(map(({ call, summary, recording }) => {
|
|
1992
|
+
if (!isHistoryCallDto(call))
|
|
1993
|
+
return { data: null };
|
|
1994
|
+
return {
|
|
1995
|
+
data: mapHistoryDetailDto(call, summary, recording?.url),
|
|
1996
|
+
};
|
|
1997
|
+
}), catchError(() => of({ data: null })));
|
|
1998
|
+
}
|
|
1999
|
+
regenerateCallSummary(callId) {
|
|
2000
|
+
if (this.useMock)
|
|
2001
|
+
return this.mock.regenerateCallSummary(callId);
|
|
2002
|
+
const enc = encodeURIComponent(callId);
|
|
2003
|
+
return this.http
|
|
2004
|
+
.post(`${this.arBase}/calls/${enc}/summary/regenerate`, {})
|
|
2005
|
+
.pipe(map((body) => ({ data: mapRegenerateSummaryResponse(body) })), catchError((err) => throwError(() => ({
|
|
2006
|
+
message: this.apiErrorMessage(err, 'Failed to regenerate summary.'),
|
|
2007
|
+
status: err.status,
|
|
2008
|
+
}))));
|
|
2009
|
+
}
|
|
2010
|
+
postTakeover(id) {
|
|
2011
|
+
if (this.useMock)
|
|
2012
|
+
return this.mock.postTakeover(id);
|
|
2013
|
+
return this.postCallAction(id, 'takeover');
|
|
2014
|
+
}
|
|
2015
|
+
postResumeAi(id) {
|
|
2016
|
+
if (this.useMock)
|
|
2017
|
+
return this.mock.postResumeAi(id);
|
|
2018
|
+
return this.postCallAction(id, 'resume-ai');
|
|
2019
|
+
}
|
|
2020
|
+
postPause(id) {
|
|
2021
|
+
if (this.useMock)
|
|
2022
|
+
return this.mock.postPause(id);
|
|
2023
|
+
return this.postCallAction(id, 'pause');
|
|
2024
|
+
}
|
|
2025
|
+
postTransfer(id) {
|
|
2026
|
+
if (this.useMock)
|
|
2027
|
+
return this.mock.postTransfer(id);
|
|
2028
|
+
return this.postCallAction(id, 'transfer');
|
|
2029
|
+
}
|
|
2030
|
+
postEnd(id) {
|
|
2031
|
+
if (this.useMock)
|
|
2032
|
+
return this.mock.postEnd(id);
|
|
2033
|
+
const enc = encodeURIComponent(id);
|
|
2034
|
+
return this.http
|
|
2035
|
+
.post(`${this.arBase}/calls/${enc}/end`, {})
|
|
2036
|
+
.pipe(map(() => ({ data: { success: true } })));
|
|
2037
|
+
}
|
|
2038
|
+
postMessage(id, text) {
|
|
2039
|
+
if (this.useMock)
|
|
2040
|
+
return this.mock.postMessage(id, text);
|
|
2041
|
+
const enc = encodeURIComponent(id);
|
|
2042
|
+
return this.http
|
|
2043
|
+
.post(`${this.arBase}/calls/${enc}/messages`, { text })
|
|
2044
|
+
.pipe(map(() => ({ data: { success: true } })));
|
|
2045
|
+
}
|
|
2046
|
+
postReview(id) {
|
|
2047
|
+
if (this.useMock)
|
|
2048
|
+
return this.mock.postReview(id);
|
|
2049
|
+
const enc = encodeURIComponent(id);
|
|
2050
|
+
return this.http
|
|
2051
|
+
.post(`${this.arBase}/calls/${enc}/review`, {})
|
|
2052
|
+
.pipe(map(() => ({ data: { review: 'reviewed' } })));
|
|
2053
|
+
}
|
|
2054
|
+
getTasks(filters) {
|
|
2055
|
+
if (this.useMock)
|
|
2056
|
+
return this.mock.getTasks(filters);
|
|
2057
|
+
let params = new HttpParams();
|
|
2058
|
+
if (filters?.status) {
|
|
2059
|
+
const apiStatus = filters.status === 'in_progress' ? 'in-progress' : filters.status;
|
|
2060
|
+
params = params.set('status', apiStatus);
|
|
2061
|
+
}
|
|
2062
|
+
if (filters?.type)
|
|
2063
|
+
params = params.set('type', filters.type);
|
|
2064
|
+
if (filters?.source)
|
|
2065
|
+
params = params.set('source', filters.source);
|
|
2066
|
+
if (filters?.callId)
|
|
2067
|
+
params = params.set('callId', filters.callId);
|
|
2068
|
+
if (filters?.limit != null)
|
|
2069
|
+
params = params.set('limit', String(filters.limit));
|
|
2070
|
+
return this.http.get(`${this.arBase}/tasks`, { params }).pipe(map((body) => ({ data: asTaskList(body).map(mapTaskDto) })));
|
|
2071
|
+
}
|
|
2072
|
+
getCallTasks(callId) {
|
|
2073
|
+
if (this.useMock)
|
|
2074
|
+
return this.mock.getCallTasks(callId);
|
|
2075
|
+
const enc = encodeURIComponent(callId);
|
|
2076
|
+
return this.http.get(`${this.arBase}/calls/${enc}/tasks`).pipe(map((body) => ({ data: asTaskList(body).map(mapTaskDto) })));
|
|
2077
|
+
}
|
|
2078
|
+
createTask(req) {
|
|
2079
|
+
if (this.useMock)
|
|
2080
|
+
return this.mock.createTask(req);
|
|
2081
|
+
return this.http
|
|
2082
|
+
.post(`${this.arBase}/tasks`, mapCreateTaskRequest(req))
|
|
2083
|
+
.pipe(map((body) => ({ data: mapTaskDto(body) })));
|
|
2084
|
+
}
|
|
2085
|
+
patchTask(id, status) {
|
|
2086
|
+
if (this.useMock)
|
|
2087
|
+
return this.mock.patchTask(id, status);
|
|
2088
|
+
const enc = encodeURIComponent(id);
|
|
2089
|
+
return this.http
|
|
2090
|
+
.patch(`${this.arBase}/tasks/${enc}`, { status: toApiTaskStatus(status) })
|
|
2091
|
+
.pipe(map((body) => ({ data: mapTaskDto(body) })));
|
|
2092
|
+
}
|
|
2093
|
+
getAnalytics(range) {
|
|
2094
|
+
if (this.useMock)
|
|
2095
|
+
return this.mock.getAnalytics(range);
|
|
2096
|
+
const params = new HttpParams().set('range', range);
|
|
2097
|
+
return this.http.get(`${this.arBase}/analytics/summary`, { params }).pipe(map((body) => ({ data: mapAnalyticsDto(body, range) })));
|
|
2098
|
+
}
|
|
2099
|
+
getSettings() {
|
|
2100
|
+
if (this.useMock)
|
|
2101
|
+
return this.mock.getSettings();
|
|
2102
|
+
return forkJoin({
|
|
2103
|
+
settings: this.http.get(`${this.arBase}/settings`),
|
|
2104
|
+
safety: this.http.get(`${this.arBase}/settings/safety-checks`),
|
|
2105
|
+
}).pipe(map(({ settings, safety }) => ({
|
|
2106
|
+
data: mapSettingsResponse(settings, safety),
|
|
2107
|
+
})));
|
|
2108
|
+
}
|
|
2109
|
+
putSettings(next) {
|
|
2110
|
+
if (this.useMock)
|
|
2111
|
+
return this.mock.putSettings(next);
|
|
2112
|
+
return this.http
|
|
2113
|
+
.put(`${this.arBase}/settings`, { settings: mapSettingsToRequest(next) })
|
|
2114
|
+
.pipe(switchMap(() => this.getSettings()));
|
|
2115
|
+
}
|
|
2116
|
+
/** ElevenLabs spoken sample for Agent Settings voice picker. */
|
|
2117
|
+
previewVoice(voiceId) {
|
|
2118
|
+
if (this.useMock)
|
|
2119
|
+
return this.mock.previewVoice(voiceId);
|
|
2120
|
+
return this.http.post(`${this.arBase}/settings/preview-voice`, { voiceId }, {
|
|
2121
|
+
responseType: 'blob',
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
getAudit(q) {
|
|
2125
|
+
if (this.useMock)
|
|
2126
|
+
return this.mock.getAudit(q);
|
|
2127
|
+
let params = new HttpParams();
|
|
2128
|
+
if (q)
|
|
2129
|
+
params = params.set('search', q);
|
|
2130
|
+
return this.http.get(`${this.arBase}/audit`, { params }).pipe(map((body) => ({
|
|
2131
|
+
data: asAuditList(body).map((e, i) => mapAuditEventDto(e, i)),
|
|
2132
|
+
})));
|
|
2133
|
+
}
|
|
2134
|
+
getSetup() {
|
|
2135
|
+
if (this.useMock)
|
|
2136
|
+
return this.mock.getSetup();
|
|
2137
|
+
return this.http.get(`${this.arBase}/setup`).pipe(map((body) => ({ data: mapSetupDto(body) })));
|
|
2138
|
+
}
|
|
2139
|
+
activateSetup() {
|
|
2140
|
+
if (this.useMock)
|
|
2141
|
+
return this.mock.activateSetup();
|
|
2142
|
+
return this.http
|
|
2143
|
+
.post(`${this.arBase}/setup/activate`, {})
|
|
2144
|
+
.pipe(map(() => ({ data: { success: true } })));
|
|
2145
|
+
}
|
|
2146
|
+
getLocations() {
|
|
2147
|
+
if (this.useMock)
|
|
2148
|
+
return this.mock.getLocations();
|
|
2149
|
+
return this.http.get(this.paths.apiPath('/locations')).pipe(map((body) => ({ data: extractApiRows(body).map(mapLocationDto) })));
|
|
2150
|
+
}
|
|
2151
|
+
getProviders() {
|
|
2152
|
+
if (this.useMock)
|
|
2153
|
+
return this.mock.getProviders();
|
|
2154
|
+
return this.http.get(this.paths.v1Path('/providers')).pipe(map((body) => ({ data: extractApiRows(body).map(mapProviderDto) })));
|
|
2155
|
+
}
|
|
2156
|
+
getStreamLines() {
|
|
2157
|
+
return this.mock.getStreamLines();
|
|
2158
|
+
}
|
|
2159
|
+
appendStreamLine(callId, line) {
|
|
2160
|
+
this.mock.appendStreamLine(callId, line);
|
|
2161
|
+
}
|
|
2162
|
+
usesMockStreaming() {
|
|
2163
|
+
return this.useMock;
|
|
2164
|
+
}
|
|
2165
|
+
getCallApiLogs(callId) {
|
|
2166
|
+
if (this.useMock)
|
|
2167
|
+
return this.mock.getCallApiLogs(callId);
|
|
2168
|
+
const enc = encodeURIComponent(callId);
|
|
2169
|
+
return this.http.get(`${this.arBase}/calls/${enc}/api-logs`).pipe(map((body) => ({
|
|
2170
|
+
items: [...(body.items ?? [])].sort((a, b) => Date.parse(a.startedAt) - Date.parse(b.startedAt)),
|
|
2171
|
+
total: body.total ?? body.items?.length ?? 0,
|
|
2172
|
+
})));
|
|
2173
|
+
}
|
|
2174
|
+
getApiLogs(filters = {}) {
|
|
2175
|
+
if (this.useMock)
|
|
2176
|
+
return this.mock.getApiLogs(filters);
|
|
2177
|
+
let params = new HttpParams();
|
|
2178
|
+
if (filters.provider)
|
|
2179
|
+
params = params.set('provider', filters.provider);
|
|
2180
|
+
if (filters.success !== undefined)
|
|
2181
|
+
params = params.set('success', String(filters.success));
|
|
2182
|
+
if (filters.statusCode != null)
|
|
2183
|
+
params = params.set('statusCode', String(filters.statusCode));
|
|
2184
|
+
if (filters.operation)
|
|
2185
|
+
params = params.set('operation', filters.operation);
|
|
2186
|
+
if (filters.callId)
|
|
2187
|
+
params = params.set('callId', filters.callId);
|
|
2188
|
+
if (filters.from)
|
|
2189
|
+
params = params.set('from', filters.from);
|
|
2190
|
+
if (filters.to)
|
|
2191
|
+
params = params.set('to', filters.to);
|
|
2192
|
+
if (filters.search)
|
|
2193
|
+
params = params.set('search', filters.search);
|
|
2194
|
+
if (filters.page != null)
|
|
2195
|
+
params = params.set('page', String(filters.page));
|
|
2196
|
+
if (filters.limit != null)
|
|
2197
|
+
params = params.set('limit', String(filters.limit));
|
|
2198
|
+
return this.http.get(`${this.arBase}/api-logs`, { params });
|
|
2199
|
+
}
|
|
2200
|
+
getApiLogDetail(logId) {
|
|
2201
|
+
if (this.useMock)
|
|
2202
|
+
return this.mock.getApiLogDetail(logId);
|
|
2203
|
+
const enc = encodeURIComponent(logId);
|
|
2204
|
+
return this.http
|
|
2205
|
+
.get(`${this.arBase}/api-logs/${enc}`)
|
|
2206
|
+
.pipe(map((data) => ({ data })));
|
|
2207
|
+
}
|
|
2208
|
+
getApiLogStats() {
|
|
2209
|
+
if (this.useMock)
|
|
2210
|
+
return this.mock.getApiLogStats();
|
|
2211
|
+
return this.http
|
|
2212
|
+
.get(`${this.arBase}/api-logs/stats`)
|
|
2213
|
+
.pipe(map((data) => ({ data })));
|
|
2214
|
+
}
|
|
2215
|
+
getOutboundPurposes(includeInactive = false) {
|
|
2216
|
+
if (this.useMock)
|
|
2217
|
+
return this.mock.getOutboundPurposes(includeInactive);
|
|
2218
|
+
const params = includeInactive ? new HttpParams().set('includeInactive', 'true') : undefined;
|
|
2219
|
+
return this.http.get(`${this.arBase}/calls/outbound/purposes`, {
|
|
2220
|
+
params,
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
createOutboundPurpose(body) {
|
|
2224
|
+
if (this.useMock)
|
|
2225
|
+
return this.mock.createOutboundPurpose(body);
|
|
2226
|
+
return this.http.post(`${this.arBase}/calls/outbound/purposes`, body);
|
|
2227
|
+
}
|
|
2228
|
+
updateOutboundPurpose(purposeId, body) {
|
|
2229
|
+
if (this.useMock)
|
|
2230
|
+
return this.mock.updateOutboundPurpose(purposeId, body);
|
|
2231
|
+
const enc = encodeURIComponent(purposeId);
|
|
2232
|
+
return this.http.patch(`${this.arBase}/calls/outbound/purposes/${enc}`, body);
|
|
2233
|
+
}
|
|
2234
|
+
deleteOutboundPurpose(purposeId) {
|
|
2235
|
+
if (this.useMock)
|
|
2236
|
+
return this.mock.deleteOutboundPurpose(purposeId);
|
|
2237
|
+
const enc = encodeURIComponent(purposeId);
|
|
2238
|
+
return this.http.delete(`${this.arBase}/calls/outbound/purposes/${enc}`);
|
|
2239
|
+
}
|
|
2240
|
+
placeOutboundCall(body) {
|
|
2241
|
+
if (this.useMock)
|
|
2242
|
+
return this.mock.placeOutboundCall(body);
|
|
2243
|
+
return this.http.post(`${this.arBase}/calls/outbound`, body).pipe(catchError((err) => {
|
|
2244
|
+
const detail = typeof err.error === 'object' && err.error && 'message' in err.error
|
|
2245
|
+
? String(err.error.message)
|
|
2246
|
+
: err.message;
|
|
2247
|
+
return throwError(() => ({ message: detail || 'Failed to place outbound call.' }));
|
|
2248
|
+
}));
|
|
2249
|
+
}
|
|
2250
|
+
postCallAction(id, action) {
|
|
2251
|
+
const enc = encodeURIComponent(id);
|
|
2252
|
+
return this.http
|
|
2253
|
+
.post(`${this.arBase}/calls/${enc}/${action}`, {})
|
|
2254
|
+
.pipe(map(() => ({ data: { success: true } })));
|
|
2255
|
+
}
|
|
2256
|
+
apiErrorMessage(err, fallback) {
|
|
2257
|
+
const body = err.error;
|
|
2258
|
+
if (typeof body === 'object' && body && 'message' in body) {
|
|
2259
|
+
return String(body.message);
|
|
2260
|
+
}
|
|
2261
|
+
if (typeof body === 'string' && body.trim())
|
|
2262
|
+
return body;
|
|
2263
|
+
return fallback;
|
|
2264
|
+
}
|
|
2265
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2266
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, providedIn: 'root' }); }
|
|
2267
|
+
}
|
|
2268
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, decorators: [{
|
|
2269
|
+
type: Injectable,
|
|
2270
|
+
args: [{ providedIn: 'root' }]
|
|
2271
|
+
}] });
|
|
2272
|
+
|
|
2273
|
+
/**
|
|
2274
|
+
* Shared source of truth for Ava online/offline across topbar + module nav.
|
|
2275
|
+
* Always re-fetches GET /status after mutations.
|
|
2276
|
+
*/
|
|
2277
|
+
class AgentOnlineService {
|
|
2278
|
+
constructor() {
|
|
2279
|
+
this.api = inject(AiReceptionistApiService);
|
|
2280
|
+
/** true = online, false = offline, null = unknown / not loaded */
|
|
2281
|
+
this._online = signal(null);
|
|
2282
|
+
this._loading = signal(false);
|
|
2283
|
+
this._toggling = signal(false);
|
|
2284
|
+
this._error = signal(null);
|
|
2285
|
+
this._readiness = signal(null);
|
|
2286
|
+
this.online = this._online.asReadonly();
|
|
2287
|
+
this.loading = this._loading.asReadonly();
|
|
2288
|
+
this.toggling = this._toggling.asReadonly();
|
|
2289
|
+
this.error = this._error.asReadonly();
|
|
2290
|
+
this.readiness = this._readiness.asReadonly();
|
|
2291
|
+
}
|
|
2292
|
+
refresh() {
|
|
2293
|
+
this._loading.set(true);
|
|
2294
|
+
this._error.set(null);
|
|
2295
|
+
this.api.getStatus().subscribe({
|
|
2296
|
+
next: ({ data }) => {
|
|
2297
|
+
this._online.set(data.agentOn);
|
|
2298
|
+
this._readiness.set(data.readiness);
|
|
2299
|
+
this._loading.set(false);
|
|
2300
|
+
},
|
|
2301
|
+
error: (err) => {
|
|
2302
|
+
this._online.set(null);
|
|
2303
|
+
this._error.set(err.message ?? 'Could not load agent status.');
|
|
2304
|
+
this._loading.set(false);
|
|
2305
|
+
},
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* PUT /agent-online then re-fetch GET /status.
|
|
2310
|
+
*/
|
|
2311
|
+
setOnline(enabled) {
|
|
2312
|
+
this._toggling.set(true);
|
|
2313
|
+
this._error.set(null);
|
|
2314
|
+
return this.api.setAgentOnline(enabled).pipe(switchMap(() => this.api.getStatus().pipe(catchError((err) => {
|
|
2315
|
+
this._error.set(err.message ?? 'Status refresh failed after toggle.');
|
|
2316
|
+
return of(null);
|
|
2317
|
+
}))), tap((result) => {
|
|
2318
|
+
if (result) {
|
|
2319
|
+
this._online.set(result.data.agentOn);
|
|
2320
|
+
this._readiness.set(result.data.readiness);
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
// Toggle likely succeeded; use requested value until refresh works.
|
|
2324
|
+
this._online.set(enabled);
|
|
2325
|
+
}
|
|
2326
|
+
}), catchError((err) => {
|
|
2327
|
+
this._error.set(err.message ?? 'Failed to update agent online status.');
|
|
2328
|
+
return throwError(() => err);
|
|
2329
|
+
}), finalize(() => this._toggling.set(false)));
|
|
2330
|
+
}
|
|
2331
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2332
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, providedIn: 'root' }); }
|
|
2333
|
+
}
|
|
2334
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, decorators: [{
|
|
2335
|
+
type: Injectable,
|
|
2336
|
+
args: [{ providedIn: 'root' }]
|
|
2337
|
+
}] });
|
|
2338
|
+
|
|
2339
|
+
class EdsPulseDotComponent {
|
|
2340
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2341
|
+
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 }); }
|
|
2342
|
+
}
|
|
2343
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsPulseDotComponent, decorators: [{
|
|
2344
|
+
type: Component,
|
|
2345
|
+
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"] }]
|
|
2346
|
+
}] });
|
|
2347
|
+
|
|
2348
|
+
class EdsModuleNavComponent {
|
|
2349
|
+
constructor() {
|
|
2350
|
+
this.config = inject(VOICE_RECEPTIONIST_CONFIG);
|
|
2351
|
+
this.session = inject(VOICE_RECEPTIONIST_SESSION);
|
|
2352
|
+
this.agentOnline = inject(AgentOnlineService);
|
|
2353
|
+
this.agentOn = this.agentOnline.online;
|
|
2354
|
+
this.includePracticeNav = !!this.config.includePracticeNav;
|
|
2355
|
+
this.receptionistNav = [
|
|
2356
|
+
{ id: 'dashboard', label: 'Dashboard', icon: 'space_dashboard', route: 'dashboard' },
|
|
2357
|
+
{ id: 'live', label: 'Live Calls', icon: 'graphic_eq', route: 'live', pulse: true },
|
|
2358
|
+
{ id: 'history', label: 'Call History', icon: 'history', route: 'history' },
|
|
2359
|
+
{ id: 'analytics', label: 'Analytics', icon: 'monitoring', route: 'analytics' },
|
|
2360
|
+
{ id: 'knowledge', label: 'Knowledge Base', icon: 'menu_book', route: 'knowledge' },
|
|
2361
|
+
{ id: 'settings', label: 'Agent Settings', icon: 'tune', route: 'settings' },
|
|
2362
|
+
{ id: 'audit', label: 'Audit Log', icon: 'verified', route: 'audit' },
|
|
2363
|
+
];
|
|
2364
|
+
this.practiceNav = [
|
|
2365
|
+
{ id: 'patients', label: 'Patients', icon: 'groups', route: 'patients' },
|
|
2366
|
+
{ id: 'providers', label: 'Providers', icon: 'medical_services', route: 'providers' },
|
|
2367
|
+
{ id: 'appointments', label: 'Appointments', icon: 'calendar_month', route: 'appointments' },
|
|
2368
|
+
{ id: 'admin', label: 'Admin', icon: 'admin_panel_settings', route: 'admin', adminOnly: true },
|
|
2369
|
+
];
|
|
2370
|
+
if (this.agentOnline.online() === null && !this.agentOnline.loading()) {
|
|
2371
|
+
this.agentOnline.refresh();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
showNavItem(item) {
|
|
2375
|
+
return !item.adminOnly || !!this.session?.isAdmin();
|
|
2376
|
+
}
|
|
2377
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2378
|
+
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 }); }
|
|
2379
|
+
}
|
|
2380
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, decorators: [{
|
|
2381
|
+
type: Component,
|
|
2382
|
+
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"] }]
|
|
2383
|
+
}], ctorParameters: () => [] });
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Lightweight, reusable modal shell.
|
|
2387
|
+
*
|
|
2388
|
+
* Renders a centered card on top of a dimmed backdrop. Closes on backdrop
|
|
2389
|
+
* click, ESC key, or via the close button. Slot the modal body using
|
|
2390
|
+
* <ng-content>.
|
|
2391
|
+
*/
|
|
2392
|
+
class ModalComponent {
|
|
2393
|
+
constructor() {
|
|
2394
|
+
this.maxWidth = '640px';
|
|
2395
|
+
this.closed = new EventEmitter();
|
|
2396
|
+
}
|
|
2397
|
+
onEscape() {
|
|
2398
|
+
this.closed.emit();
|
|
2399
|
+
}
|
|
2400
|
+
onBackdropClick(event) {
|
|
2401
|
+
if (event.target === event.currentTarget) {
|
|
2402
|
+
this.closed.emit();
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
close() {
|
|
2406
|
+
this.closed.emit();
|
|
2407
|
+
}
|
|
2408
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2409
|
+
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 }); }
|
|
2410
|
+
}
|
|
2411
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ModalComponent, decorators: [{
|
|
2412
|
+
type: Component,
|
|
2413
|
+
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"] }]
|
|
2414
|
+
}], propDecorators: { title: [{
|
|
2415
|
+
type: Input,
|
|
2416
|
+
args: [{ required: true }]
|
|
2417
|
+
}], subtitle: [{
|
|
2418
|
+
type: Input
|
|
2419
|
+
}], maxWidth: [{
|
|
2420
|
+
type: Input
|
|
2421
|
+
}], closed: [{
|
|
2422
|
+
type: Output
|
|
2423
|
+
}], onEscape: [{
|
|
2424
|
+
type: HostListener,
|
|
2425
|
+
args: ['document:keydown.escape']
|
|
2426
|
+
}] } });
|
|
2427
|
+
|
|
2428
|
+
class EdsButtonComponent {
|
|
2429
|
+
constructor() {
|
|
2430
|
+
this.variant = input('secondary');
|
|
2431
|
+
this.type = input('button');
|
|
2432
|
+
this.disabled = input(false);
|
|
2433
|
+
}
|
|
2434
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2435
|
+
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: `
|
|
2436
|
+
<button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
|
|
2437
|
+
<ng-content />
|
|
2438
|
+
</button>
|
|
2439
|
+
`, 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 }); }
|
|
2440
|
+
}
|
|
2441
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsButtonComponent, decorators: [{
|
|
2442
|
+
type: Component,
|
|
2443
|
+
args: [{ selector: 'eds-button', standalone: true, template: `
|
|
2444
|
+
<button [type]="type()" [disabled]="disabled()" [attr.data-variant]="variant()" class="eds-btn">
|
|
2445
|
+
<ng-content />
|
|
2446
|
+
</button>
|
|
2447
|
+
`, 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"] }]
|
|
2448
|
+
}] });
|
|
2449
|
+
|
|
2450
|
+
const PAGE_TITLES = {
|
|
2451
|
+
dashboard: 'Dashboard',
|
|
2452
|
+
live: 'Live Calls',
|
|
2453
|
+
history: 'Call History',
|
|
2454
|
+
tasks: 'Tasks & Follow-ups',
|
|
2455
|
+
analytics: 'Analytics',
|
|
2456
|
+
settings: 'Agent Settings',
|
|
2457
|
+
knowledge: 'Knowledge Base',
|
|
2458
|
+
audit: 'Audit Log',
|
|
2459
|
+
wizard: 'Setup Wizard',
|
|
2460
|
+
patients: 'Patients',
|
|
2461
|
+
providers: 'Providers',
|
|
2462
|
+
appointments: 'Appointments',
|
|
2463
|
+
admin: 'Admin',
|
|
2464
|
+
};
|
|
2465
|
+
class EdsTopbarComponent {
|
|
2466
|
+
constructor() {
|
|
2467
|
+
this.router = inject(Router);
|
|
2468
|
+
this.sessionAdapter = inject(VOICE_RECEPTIONIST_SESSION);
|
|
2469
|
+
this.toast = inject(ToastService);
|
|
2470
|
+
this.agentOnline = inject(AgentOnlineService);
|
|
2471
|
+
this.pageTitle = signal('Dashboard');
|
|
2472
|
+
this.confirmOffOpen = signal(false);
|
|
2473
|
+
this.online = this.agentOnline.online;
|
|
2474
|
+
this.loading = this.agentOnline.loading;
|
|
2475
|
+
this.toggling = this.agentOnline.toggling;
|
|
2476
|
+
this.statusError = this.agentOnline.error;
|
|
2477
|
+
this.userEmail = computed(() => this.sessionAdapter?.email() ?? null);
|
|
2478
|
+
this.showUser = computed(() => !!this.userEmail());
|
|
2479
|
+
this.showLogout = computed(() => !!this.sessionAdapter);
|
|
2480
|
+
this.userInitials = computed(() => {
|
|
2481
|
+
const email = this.userEmail()?.trim() ?? '';
|
|
2482
|
+
if (!email)
|
|
2483
|
+
return '?';
|
|
2484
|
+
const local = email.split('@')[0] ?? '';
|
|
2485
|
+
const parts = local.split(/[._-]+/).filter(Boolean);
|
|
2486
|
+
if (parts.length >= 2) {
|
|
2487
|
+
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
2488
|
+
}
|
|
2489
|
+
return local.slice(0, 2).toUpperCase() || '?';
|
|
2490
|
+
});
|
|
2491
|
+
this.statusLabel = computed(() => {
|
|
2492
|
+
if (this.loading() && this.online() === null)
|
|
2493
|
+
return 'Checking status…';
|
|
2494
|
+
if (this.online() === null)
|
|
2495
|
+
return 'Status unknown';
|
|
2496
|
+
return this.online()
|
|
2497
|
+
? 'AI receptionist is answering calls'
|
|
2498
|
+
: 'AI is offline — calls go to front desk';
|
|
2499
|
+
});
|
|
2500
|
+
this.switchDisabled = computed(() => this.loading() || this.toggling() || this.online() === null);
|
|
2501
|
+
this.agentOnline.refresh();
|
|
2502
|
+
this.updateTitle(this.router.url);
|
|
2503
|
+
this.router.events
|
|
2504
|
+
.pipe(filter((e) => e instanceof NavigationEnd))
|
|
2505
|
+
.subscribe((e) => this.updateTitle(e.urlAfterRedirects));
|
|
2506
|
+
}
|
|
2507
|
+
onToggleClick() {
|
|
2508
|
+
if (this.switchDisabled())
|
|
2509
|
+
return;
|
|
2510
|
+
if (this.online()) {
|
|
2511
|
+
this.confirmOffOpen.set(true);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
this.applyOnline(true);
|
|
2515
|
+
}
|
|
2516
|
+
cancelConfirmOff() {
|
|
2517
|
+
this.confirmOffOpen.set(false);
|
|
2518
|
+
}
|
|
2519
|
+
confirmTurnOff() {
|
|
2520
|
+
this.confirmOffOpen.set(false);
|
|
2521
|
+
this.applyOnline(false);
|
|
2522
|
+
}
|
|
2523
|
+
applyOnline(enabled) {
|
|
2524
|
+
this.agentOnline.setOnline(enabled).subscribe({
|
|
2525
|
+
next: () => {
|
|
2526
|
+
this.toast.show(enabled
|
|
2527
|
+
? 'AI receptionist is online — Ava will answer inbound calls.'
|
|
2528
|
+
: 'AI receptionist is offline — calls go to the front desk.', 'success');
|
|
2529
|
+
},
|
|
2530
|
+
error: (err) => {
|
|
2531
|
+
this.toast.show(err.message ?? 'Failed to update agent status.', 'error');
|
|
2532
|
+
this.agentOnline.refresh();
|
|
2533
|
+
},
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
logout() {
|
|
2537
|
+
this.sessionAdapter?.logout();
|
|
2538
|
+
}
|
|
2539
|
+
updateTitle(url) {
|
|
2540
|
+
const base = url.split('?')[0].split(';')[0];
|
|
2541
|
+
const segments = base.split('/').filter(Boolean);
|
|
2542
|
+
const last = segments[segments.length - 1] ?? '';
|
|
2543
|
+
const prev = segments[segments.length - 2] ?? '';
|
|
2544
|
+
if (prev === 'live' && last && last !== 'live') {
|
|
2545
|
+
this.pageTitle.set('Live Monitor');
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (prev === 'history' && last && last !== 'history') {
|
|
2549
|
+
this.pageTitle.set('Call Detail');
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (prev === 'settings' && last === 'outbound-purposes') {
|
|
2553
|
+
this.pageTitle.set('Outbound Purposes');
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
this.pageTitle.set(PAGE_TITLES[last] ?? 'AI Receptionist');
|
|
2557
|
+
}
|
|
2558
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2559
|
+
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 }); }
|
|
2560
|
+
}
|
|
2561
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsTopbarComponent, decorators: [{
|
|
2562
|
+
type: Component,
|
|
2563
|
+
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"] }]
|
|
2564
|
+
}], ctorParameters: () => [] });
|
|
2565
|
+
|
|
2566
|
+
class EdsAppShellComponent {
|
|
2567
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2568
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: EdsAppShellComponent, isStandalone: true, selector: "eds-app-shell", ngImport: i0, template: "<div class=\"eds-layout\">\n <eds-app-rail />\n <eds-module-nav />\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: EdsAppRailComponent, selector: "eds-app-rail" }, { 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 }); }
|
|
2569
|
+
}
|
|
2570
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsAppShellComponent, decorators: [{
|
|
2571
|
+
type: Component,
|
|
2572
|
+
args: [{ selector: 'eds-app-shell', imports: [
|
|
2573
|
+
RouterOutlet,
|
|
2574
|
+
EdsAppRailComponent,
|
|
2575
|
+
EdsModuleNavComponent,
|
|
2576
|
+
EdsTopbarComponent,
|
|
2577
|
+
ToastContainerComponent,
|
|
2578
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-layout\">\n <eds-app-rail />\n <eds-module-nav />\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"] }]
|
|
2579
|
+
}] });
|
|
2580
|
+
|
|
2581
|
+
/** Feature child routes (under the embed shell). */
|
|
2582
|
+
const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
2583
|
+
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
2584
|
+
{
|
|
2585
|
+
path: 'dashboard',
|
|
2586
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs').then((m) => m.DashboardPageComponent),
|
|
2587
|
+
},
|
|
2588
|
+
{
|
|
2589
|
+
path: 'live',
|
|
2590
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs').then((m) => m.LiveCallsPageComponent),
|
|
2591
|
+
},
|
|
2592
|
+
{
|
|
2593
|
+
path: 'live/:id',
|
|
2594
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs').then((m) => m.LiveMonitorPageComponent),
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
path: 'history',
|
|
2598
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs').then((m) => m.CallHistoryPageComponent),
|
|
2599
|
+
},
|
|
2600
|
+
{
|
|
2601
|
+
path: 'history/:id',
|
|
2602
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs').then((m) => m.CallHistoryDetailPageComponent),
|
|
2603
|
+
},
|
|
2604
|
+
{
|
|
2605
|
+
path: 'tasks',
|
|
2606
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs').then((m) => m.TasksPageComponent),
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
path: 'analytics',
|
|
2610
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs').then((m) => m.AnalyticsPageComponent),
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
path: 'settings',
|
|
2614
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs').then((m) => m.SettingsPageComponent),
|
|
2615
|
+
},
|
|
2616
|
+
{
|
|
2617
|
+
path: 'settings/outbound-purposes',
|
|
2618
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs').then((m) => m.OutboundPurposesPageComponent),
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
path: 'knowledge',
|
|
2622
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs').then((m) => m.KnowledgeHubPageComponent),
|
|
2623
|
+
},
|
|
2624
|
+
{
|
|
2625
|
+
path: 'audit',
|
|
2626
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs').then((m) => m.AuditPageComponent),
|
|
2627
|
+
},
|
|
2628
|
+
{
|
|
2629
|
+
path: 'wizard',
|
|
2630
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs').then((m) => m.WizardPageComponent),
|
|
2631
|
+
},
|
|
2632
|
+
];
|
|
2633
|
+
/**
|
|
2634
|
+
* Lazy-loadable routes for EHR (and standalone hosts).
|
|
2635
|
+
* Mount under a host path, e.g. `{ path: 'ai-receptionist', children: VOICE_RECEPTIONIST_ROUTES }`.
|
|
2636
|
+
*/
|
|
2637
|
+
const VOICE_RECEPTIONIST_ROUTES = [
|
|
2638
|
+
{
|
|
2639
|
+
path: '',
|
|
2640
|
+
component: EdsAppShellComponent,
|
|
2641
|
+
children: VOICE_RECEPTIONIST_FEATURE_ROUTES,
|
|
2642
|
+
},
|
|
2643
|
+
];
|
|
2644
|
+
|
|
2645
|
+
class EdsBadgeComponent {
|
|
2646
|
+
constructor() {
|
|
2647
|
+
this.tone = input('gray');
|
|
2648
|
+
}
|
|
2649
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2650
|
+
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 }); }
|
|
2651
|
+
}
|
|
2652
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsBadgeComponent, decorators: [{
|
|
2653
|
+
type: Component,
|
|
2654
|
+
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"] }]
|
|
2655
|
+
}] });
|
|
2656
|
+
|
|
2657
|
+
/*
|
|
2658
|
+
* Public API Surface of @edvak99/voice-receptionist-widget
|
|
2659
|
+
*/
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* Generated bundle index. Do not edit.
|
|
2663
|
+
*/
|
|
2664
|
+
|
|
2665
|
+
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 };
|
|
2666
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs.map
|