@edvak99/voice-receptionist-widget 1.1.24 → 1.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs +533 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-analytics-page.component-CgvFQGH2.mjs → edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-analytics-page.component-CgvFQGH2.mjs.map → edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs +96 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-B5fFK6Qy.mjs → edvak99-voice-receptionist-widget-call-history-detail-page.component-pRSrt5C2.mjs} +22 -32
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-pRSrt5C2.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs +254 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-D9hxkypQ.mjs +110 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-D9hxkypQ.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DheHXZ3u.mjs → edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DheHXZ3u.mjs.map → edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs +311 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DKMxM5TC.mjs → edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs} +177 -105
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-page.component-B4qH9J7y.mjs → edvak99-voice-receptionist-widget-live-calls-page.component-BLJTOY-p.mjs} +4 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-page.component-B4qH9J7y.mjs.map → edvak99-voice-receptionist-widget-live-calls-page.component-BLJTOY-p.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-D-Y1NJfR.mjs → edvak99-voice-receptionist-widget-live-monitor-page.component-MS9bpOBv.mjs} +3 -3
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-D-Y1NJfR.mjs.map → edvak99-voice-receptionist-widget-live-monitor-page.component-MS9bpOBv.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Dc9PJDhp.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs} +4 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Dc9PJDhp.mjs.map → edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-D3xwM7DS.mjs → edvak99-voice-receptionist-widget-task-detail-drawer.component-CAsLgIJB.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-D3xwM7DS.mjs.map → edvak99-voice-receptionist-widget-task-detail-drawer.component-CAsLgIJB.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-Clfvj0eo.mjs → edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-BKEN5NGh.mjs → edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-BKEN5NGh.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/components/agent-settings-drawer/agent-settings-drawer.component.d.ts +0 -1
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +3 -4
- package/lib/layout/eds-module-nav.component.d.ts +7 -2
- package/lib/models/ai-receptionist.types.d.ts +4 -4
- package/lib/pages/activities-page/activities-page.component.d.ts +0 -1
- package/lib/pages/audit-page/audit-page.component.d.ts +2 -0
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +4 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +11 -12
- package/lib/services/ai-receptionist-api.mappers.d.ts +1 -0
- package/lib/services/ai-receptionist-api.mock.d.ts +1 -1
- package/lib/services/ai-receptionist-api.service.d.ts +3 -2
- package/package.json +1 -1
- package/src/assets/css/_tables.scss +0 -12
- package/src/assets/icons/Headset.svg +5 -0
- package/src/assets/icons/ai_headset.svg +5 -0
- package/styles/embed.scss +0 -96
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-8iyWFmmj.mjs +0 -539
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-8iyWFmmj.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-Bhb-n_aA.mjs +0 -89
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-Bhb-n_aA.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-B5fFK6Qy.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-pZpvcGXo.mjs +0 -254
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-pZpvcGXo.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-DvPYib0Q.mjs +0 -110
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-DvPYib0Q.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DYr-Wfn9.mjs +0 -311
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DYr-Wfn9.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DKMxM5TC.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Clfvj0eo.mjs.map +0 -1
- package/src/assets/icons/ai_headset_badge.svg +0 -25
|
@@ -1221,7 +1221,6 @@ function humanizeSettingsKey(key) {
|
|
|
1221
1221
|
.trim()
|
|
1222
1222
|
.replace(/^\w/, (c) => c.toUpperCase());
|
|
1223
1223
|
}
|
|
1224
|
-
/** Map frontend AgentSettings → backend PUT body section keys. */
|
|
1225
1224
|
const DEFAULT_BOOKING_MODE_OPTIONS = [
|
|
1226
1225
|
{
|
|
1227
1226
|
value: 'confirmed',
|
|
@@ -1234,8 +1233,23 @@ const DEFAULT_BOOKING_MODE_OPTIONS = [
|
|
|
1234
1233
|
description: 'Submits an Online Request for staff review (req-auto-new-appointment). Do not tell the caller the visit is confirmed.',
|
|
1235
1234
|
},
|
|
1236
1235
|
];
|
|
1236
|
+
function unwrapBookingModePayload(body) {
|
|
1237
|
+
if (!body || typeof body !== 'object')
|
|
1238
|
+
return {};
|
|
1239
|
+
const raw = body;
|
|
1240
|
+
if (typeof raw['bookingMode'] === 'string' || Array.isArray(raw['options'])) {
|
|
1241
|
+
return raw;
|
|
1242
|
+
}
|
|
1243
|
+
if (raw['data'] && typeof raw['data'] === 'object') {
|
|
1244
|
+
return unwrapBookingModePayload(raw['data']);
|
|
1245
|
+
}
|
|
1246
|
+
if (raw['settings'] && typeof raw['settings'] === 'object') {
|
|
1247
|
+
return unwrapBookingModePayload(raw['settings']);
|
|
1248
|
+
}
|
|
1249
|
+
return raw;
|
|
1250
|
+
}
|
|
1237
1251
|
function mapBookingModeSettings(body) {
|
|
1238
|
-
const raw = (body
|
|
1252
|
+
const raw = unwrapBookingModePayload(body);
|
|
1239
1253
|
const mode = String(raw.bookingMode ?? '').trim().toLowerCase();
|
|
1240
1254
|
const bookingMode = mode === 'pending' ? 'pending' : 'confirmed';
|
|
1241
1255
|
const options = Array.isArray(raw.options) && raw.options.length
|
|
@@ -1258,6 +1272,7 @@ function mapBookingModeSettings(body) {
|
|
|
1258
1272
|
options: options.length ? options : DEFAULT_BOOKING_MODE_OPTIONS,
|
|
1259
1273
|
};
|
|
1260
1274
|
}
|
|
1275
|
+
/** Map frontend AgentSettings → backend PUT body section keys. */
|
|
1261
1276
|
function mapSettingsToRequest(settings) {
|
|
1262
1277
|
return {
|
|
1263
1278
|
general: settings.general,
|
|
@@ -3408,7 +3423,7 @@ class AiReceptionistApiService {
|
|
|
3408
3423
|
}
|
|
3409
3424
|
/**
|
|
3410
3425
|
* Practice-level Confirm vs Pending booking mode.
|
|
3411
|
-
* Dedicated endpoint
|
|
3426
|
+
* Dedicated endpoint — JWT supplies the practice; do not send practiceId.
|
|
3412
3427
|
*/
|
|
3413
3428
|
getBookingMode() {
|
|
3414
3429
|
if (this.useMock)
|
|
@@ -3742,12 +3757,30 @@ class AiReceptionistApiService {
|
|
|
3742
3757
|
apiErrorMessage(err, fallback) {
|
|
3743
3758
|
const body = err.error;
|
|
3744
3759
|
if (typeof body === 'object' && body && 'message' in body) {
|
|
3745
|
-
|
|
3760
|
+
const text = this.coerceErrorText(body.message);
|
|
3761
|
+
if (text)
|
|
3762
|
+
return text;
|
|
3763
|
+
}
|
|
3764
|
+
if (typeof body === 'string' && body.trim() && body !== '[object Object]')
|
|
3765
|
+
return body.trim();
|
|
3766
|
+
if (typeof err.message === 'string' && err.message.trim() && err.message !== '[object Object]') {
|
|
3767
|
+
return err.message.trim();
|
|
3746
3768
|
}
|
|
3747
|
-
if (typeof body === 'string' && body.trim())
|
|
3748
|
-
return body;
|
|
3749
3769
|
return fallback;
|
|
3750
3770
|
}
|
|
3771
|
+
coerceErrorText(value) {
|
|
3772
|
+
if (typeof value === 'string' && value.trim() && value !== '[object Object]') {
|
|
3773
|
+
return value.trim();
|
|
3774
|
+
}
|
|
3775
|
+
if (Array.isArray(value)) {
|
|
3776
|
+
const parts = value.map((item) => this.coerceErrorText(item)).filter((item) => !!item);
|
|
3777
|
+
return parts.length ? parts.join('; ') : null;
|
|
3778
|
+
}
|
|
3779
|
+
if (value && typeof value === 'object' && 'message' in value) {
|
|
3780
|
+
return this.coerceErrorText(value.message);
|
|
3781
|
+
}
|
|
3782
|
+
return null;
|
|
3783
|
+
}
|
|
3751
3784
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3752
3785
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, providedIn: 'root' }); }
|
|
3753
3786
|
}
|
|
@@ -3857,11 +3890,11 @@ class ToastContainerComponent {
|
|
|
3857
3890
|
this.toastService = inject(ToastService);
|
|
3858
3891
|
}
|
|
3859
3892
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3860
|
-
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=\"fixed bottom-6 right-6 z-[
|
|
3893
|
+
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=\"fixed bottom-6 right-6 z-[1100] flex max-w-[380px] flex-col gap-3 pointer-events-none\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div\n class=\"toast pointer-events-auto flex items-start gap-3 rounded-lg border border-base bg-white px-4 py-3 text-[13px] text-dark shadow-[0_6px_24px_rgba(20,30,50,0.12)]\"\n role=\"status\"\n >\n <span\n class=\"inline-flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full\"\n [class]=\"toast.variant === 'success' ? 'bg-emerald-50 text-emerald-600' : toast.variant === 'error' ? 'bg-red-50 text-red-600' : 'bg-blue-50 text-blue-600'\"\n aria-hidden=\"true\"\n >\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=\"flex-1 leading-snug\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-none bg-transparent text-light transition-colors duration-150 hover:bg-lighter hover:text-dark\"\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: [".toast{animation:slide-in .16s ease}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3861
3894
|
}
|
|
3862
3895
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
3863
3896
|
type: Component,
|
|
3864
|
-
args: [{ selector: 'app-toast-container', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fixed bottom-6 right-6 z-[
|
|
3897
|
+
args: [{ selector: 'app-toast-container', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fixed bottom-6 right-6 z-[1100] flex max-w-[380px] flex-col gap-3 pointer-events-none\" aria-live=\"polite\" aria-atomic=\"false\">\n @for (toast of toastService.toasts(); track toast.id) {\n <div\n class=\"toast pointer-events-auto flex items-start gap-3 rounded-lg border border-base bg-white px-4 py-3 text-[13px] text-dark shadow-[0_6px_24px_rgba(20,30,50,0.12)]\"\n role=\"status\"\n >\n <span\n class=\"inline-flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full\"\n [class]=\"toast.variant === 'success' ? 'bg-emerald-50 text-emerald-600' : toast.variant === 'error' ? 'bg-red-50 text-red-600' : 'bg-blue-50 text-blue-600'\"\n aria-hidden=\"true\"\n >\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=\"flex-1 leading-snug\">{{ toast.text }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-none bg-transparent text-light transition-colors duration-150 hover:bg-lighter hover:text-dark\"\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: [".toast{animation:slide-in .16s ease}@keyframes slide-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
3865
3898
|
}] });
|
|
3866
3899
|
|
|
3867
3900
|
/** Coerce `[loading]` so attributes like `loading`, `loading="true"|"false"`, or a label work. */
|
|
@@ -4162,11 +4195,16 @@ class IconComponent {
|
|
|
4162
4195
|
// 2) De-dupe concurrent requests
|
|
4163
4196
|
let p = IconComponent.inflight.get(svgPath);
|
|
4164
4197
|
if (!p) {
|
|
4165
|
-
p = fetch(svgPath, { cache: '
|
|
4166
|
-
.then((res) => {
|
|
4198
|
+
p = fetch(svgPath, { cache: 'no-cache' })
|
|
4199
|
+
.then(async (res) => {
|
|
4167
4200
|
if (!res.ok)
|
|
4168
4201
|
throw new Error(`HTTP ${res.status} for ${svgPath}`);
|
|
4169
|
-
|
|
4202
|
+
const text = await res.text();
|
|
4203
|
+
// SPA fallback can return index.html with 200 for missing icons.
|
|
4204
|
+
if (!text.includes('<svg')) {
|
|
4205
|
+
throw new Error(`Not an SVG for ${svgPath}`);
|
|
4206
|
+
}
|
|
4207
|
+
return text;
|
|
4170
4208
|
})
|
|
4171
4209
|
.then((text) => {
|
|
4172
4210
|
IconComponent.cache.set(svgPath, text);
|
|
@@ -5143,13 +5181,14 @@ class SettingsPageComponent {
|
|
|
5143
5181
|
this.locations = signal([]);
|
|
5144
5182
|
this.locationsMenuOpen = signal(false);
|
|
5145
5183
|
this.confirmOffOpen = signal(false);
|
|
5184
|
+
this.voicePreviewBusy = signal(false);
|
|
5185
|
+
this.voicePreviewPlaying = signal(false);
|
|
5186
|
+
/** Isolated from main settings body — practice-level EHR booking write path. */
|
|
5146
5187
|
this.bookingMode = signal('confirmed');
|
|
5147
5188
|
this.bookingModeOptions = signal([]);
|
|
5148
5189
|
this.bookingModeLoading = signal(true);
|
|
5149
|
-
this.bookingModeSaving = signal(false);
|
|
5150
5190
|
this.bookingModeError = signal(null);
|
|
5151
|
-
this.
|
|
5152
|
-
this.voicePreviewPlaying = signal(false);
|
|
5191
|
+
this.originalBookingMode = null;
|
|
5153
5192
|
this.online = this.agentOnline.online;
|
|
5154
5193
|
this.agentLoading = this.agentOnline.loading;
|
|
5155
5194
|
this.toggling = this.agentOnline.toggling;
|
|
@@ -5254,46 +5293,64 @@ class SettingsPageComponent {
|
|
|
5254
5293
|
fieldIsReadOnly(section, key) {
|
|
5255
5294
|
return isFieldReadOnly(section, key);
|
|
5256
5295
|
}
|
|
5296
|
+
sectionIsReadOnly(section) {
|
|
5297
|
+
return READ_ONLY_SECTIONS.has(section);
|
|
5298
|
+
}
|
|
5299
|
+
isTabDisabled(id) {
|
|
5300
|
+
return DISABLED_SETTINGS_TABS.has(id);
|
|
5301
|
+
}
|
|
5302
|
+
setTab(id) {
|
|
5303
|
+
if (this.isTabDisabled(id))
|
|
5304
|
+
return;
|
|
5305
|
+
this.locationsMenuOpen.set(false);
|
|
5306
|
+
this.tab.set(id);
|
|
5307
|
+
}
|
|
5308
|
+
/** Restore last loaded/saved settings (used when Cancel/close drawer without saving). */
|
|
5309
|
+
discardChanges() {
|
|
5310
|
+
if (!this.originalSettings)
|
|
5311
|
+
return;
|
|
5312
|
+
this.settings.set(structuredClone(this.originalSettings));
|
|
5313
|
+
if (this.originalBookingMode)
|
|
5314
|
+
this.bookingMode.set(this.originalBookingMode);
|
|
5315
|
+
this.bookingModeError.set(null);
|
|
5316
|
+
this.locationsMenuOpen.set(false);
|
|
5317
|
+
this.stopVoicePreview();
|
|
5318
|
+
}
|
|
5257
5319
|
loadBookingMode() {
|
|
5258
5320
|
this.bookingModeLoading.set(true);
|
|
5259
5321
|
this.bookingModeError.set(null);
|
|
5260
5322
|
this.api.getBookingMode().subscribe({
|
|
5261
5323
|
next: ({ data }) => {
|
|
5262
5324
|
this.bookingMode.set(data.bookingMode);
|
|
5325
|
+
this.originalBookingMode = data.bookingMode;
|
|
5263
5326
|
this.bookingModeOptions.set(data.options);
|
|
5264
5327
|
this.bookingModeLoading.set(false);
|
|
5265
5328
|
},
|
|
5266
5329
|
error: (err) => {
|
|
5267
5330
|
this.bookingModeLoading.set(false);
|
|
5331
|
+
if (!this.bookingModeOptions().length) {
|
|
5332
|
+
this.bookingModeOptions.set([
|
|
5333
|
+
{
|
|
5334
|
+
value: 'confirmed',
|
|
5335
|
+
label: 'Confirm appointment',
|
|
5336
|
+
description: 'Creates a live appointment in the EHR (create-new-appointment). The caller hears that they are booked.',
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
value: 'pending',
|
|
5340
|
+
label: 'Pending appointment',
|
|
5341
|
+
description: 'Submits an Online Request for staff review (req-auto-new-appointment). Do not tell the caller the visit is confirmed.',
|
|
5342
|
+
},
|
|
5343
|
+
]);
|
|
5344
|
+
}
|
|
5268
5345
|
this.bookingModeError.set(this.asErrorText(err, 'Could not load booking mode for this practice.'));
|
|
5269
5346
|
},
|
|
5270
5347
|
});
|
|
5271
5348
|
}
|
|
5272
5349
|
selectBookingMode(mode) {
|
|
5273
|
-
if (this.
|
|
5350
|
+
if (this.saving() || this.bookingMode() === mode)
|
|
5274
5351
|
return;
|
|
5275
|
-
const previous = this.bookingMode();
|
|
5276
5352
|
this.bookingMode.set(mode);
|
|
5277
5353
|
this.bookingModeError.set(null);
|
|
5278
|
-
this.bookingModeSaving.set(true);
|
|
5279
|
-
this.api.putBookingMode(mode).subscribe({
|
|
5280
|
-
next: ({ data }) => {
|
|
5281
|
-
this.bookingMode.set(data.bookingMode);
|
|
5282
|
-
if (data.options?.length)
|
|
5283
|
-
this.bookingModeOptions.set(data.options);
|
|
5284
|
-
this.bookingModeSaving.set(false);
|
|
5285
|
-
this.toast.show(data.bookingMode === 'pending'
|
|
5286
|
-
? 'Pending appointment mode saved for this practice'
|
|
5287
|
-
: 'Confirm appointment mode saved for this practice', 'success');
|
|
5288
|
-
},
|
|
5289
|
-
error: (err) => {
|
|
5290
|
-
this.bookingMode.set(previous);
|
|
5291
|
-
this.bookingModeSaving.set(false);
|
|
5292
|
-
const text = this.asErrorText(err, 'Failed to save booking mode');
|
|
5293
|
-
this.bookingModeError.set(text);
|
|
5294
|
-
this.toast.show(text, 'error');
|
|
5295
|
-
},
|
|
5296
|
-
});
|
|
5297
5354
|
}
|
|
5298
5355
|
asErrorText(err, fallback) {
|
|
5299
5356
|
if (err instanceof Error && err.message && err.message !== '[object Object]') {
|
|
@@ -5303,33 +5360,29 @@ class SettingsPageComponent {
|
|
|
5303
5360
|
return err.trim();
|
|
5304
5361
|
if (err && typeof err === 'object') {
|
|
5305
5362
|
const o = err;
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5363
|
+
const fromMessage = this.coerceUnknownMessage(o.message);
|
|
5364
|
+
if (fromMessage)
|
|
5365
|
+
return fromMessage;
|
|
5366
|
+
const fromError = this.coerceUnknownMessage(o.error);
|
|
5367
|
+
if (fromError)
|
|
5368
|
+
return fromError;
|
|
5311
5369
|
}
|
|
5312
5370
|
return fallback;
|
|
5313
5371
|
}
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
return;
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
if (!this.originalSettings)
|
|
5329
|
-
return;
|
|
5330
|
-
this.settings.set(structuredClone(this.originalSettings));
|
|
5331
|
-
this.locationsMenuOpen.set(false);
|
|
5332
|
-
this.stopVoicePreview();
|
|
5372
|
+
coerceUnknownMessage(value) {
|
|
5373
|
+
if (typeof value === 'string' && value.trim() && value !== '[object Object]') {
|
|
5374
|
+
return value.trim();
|
|
5375
|
+
}
|
|
5376
|
+
if (Array.isArray(value)) {
|
|
5377
|
+
const parts = value
|
|
5378
|
+
.map((item) => this.coerceUnknownMessage(item))
|
|
5379
|
+
.filter((item) => !!item);
|
|
5380
|
+
return parts.length ? parts.join('; ') : null;
|
|
5381
|
+
}
|
|
5382
|
+
if (value && typeof value === 'object' && 'message' in value) {
|
|
5383
|
+
return this.coerceUnknownMessage(value.message);
|
|
5384
|
+
}
|
|
5385
|
+
return null;
|
|
5333
5386
|
}
|
|
5334
5387
|
updateField(section, key, value) {
|
|
5335
5388
|
const current = this.settings();
|
|
@@ -5357,13 +5410,6 @@ class SettingsPageComponent {
|
|
|
5357
5410
|
if (!this.locations().length)
|
|
5358
5411
|
return;
|
|
5359
5412
|
this.locationsMenuOpen.update((open) => !open);
|
|
5360
|
-
if (this.locationsMenuOpen()) {
|
|
5361
|
-
queueMicrotask(() => {
|
|
5362
|
-
document
|
|
5363
|
-
.querySelector('[data-locations-dd]')
|
|
5364
|
-
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
5365
|
-
});
|
|
5366
|
-
}
|
|
5367
5413
|
}
|
|
5368
5414
|
onLocationsDocClick(event) {
|
|
5369
5415
|
const target = event.target;
|
|
@@ -5556,25 +5602,48 @@ class SettingsPageComponent {
|
|
|
5556
5602
|
payload.transcripts['retainDays'] = Math.max(recDays, txDays);
|
|
5557
5603
|
this.restoreReadOnlyFields(payload, orig);
|
|
5558
5604
|
this.saving.set(true);
|
|
5605
|
+
this.bookingModeError.set(null);
|
|
5606
|
+
const nextBookingMode = this.bookingMode();
|
|
5607
|
+
const bookingModeDirty = this.originalBookingMode != null && nextBookingMode !== this.originalBookingMode;
|
|
5559
5608
|
this.api.putSettings(payload).subscribe({
|
|
5560
5609
|
next: ({ data }) => {
|
|
5561
5610
|
this.originalSettings = structuredClone(data);
|
|
5562
5611
|
this.settings.set(structuredClone(data));
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
this.
|
|
5612
|
+
if (!bookingModeDirty) {
|
|
5613
|
+
this.finishSaveSuccess();
|
|
5614
|
+
return;
|
|
5615
|
+
}
|
|
5616
|
+
this.api.putBookingMode(nextBookingMode).subscribe({
|
|
5617
|
+
next: ({ data: booking }) => {
|
|
5618
|
+
this.bookingMode.set(booking.bookingMode);
|
|
5619
|
+
this.originalBookingMode = booking.bookingMode;
|
|
5620
|
+
if (booking.options?.length)
|
|
5621
|
+
this.bookingModeOptions.set(booking.options);
|
|
5622
|
+
this.finishSaveSuccess();
|
|
5623
|
+
},
|
|
5624
|
+
error: (err) => {
|
|
5625
|
+
this.saving.set(false);
|
|
5626
|
+
const text = this.asErrorText(err, 'Failed to save booking mode');
|
|
5627
|
+
this.bookingModeError.set(text);
|
|
5628
|
+
this.tab.set('phone');
|
|
5629
|
+
this.toast.show(`Agent settings saved, but booking mode failed: ${text}`, 'error');
|
|
5630
|
+
},
|
|
5631
|
+
});
|
|
5568
5632
|
},
|
|
5569
5633
|
error: (err) => {
|
|
5570
5634
|
this.saving.set(false);
|
|
5571
|
-
const message = err
|
|
5572
|
-
? err.message
|
|
5573
|
-
: 'Failed to save agent settings';
|
|
5635
|
+
const message = this.asErrorText(err, 'Failed to save agent settings');
|
|
5574
5636
|
this.toast.show(message, 'error');
|
|
5575
5637
|
},
|
|
5576
5638
|
});
|
|
5577
5639
|
}
|
|
5640
|
+
finishSaveSuccess() {
|
|
5641
|
+
this.saving.set(false);
|
|
5642
|
+
this.saved.set(true);
|
|
5643
|
+
this.toast.show('Agent settings saved', 'success');
|
|
5644
|
+
setTimeout(() => this.saved.set(false), 2000);
|
|
5645
|
+
this.savedSuccessfully.emit();
|
|
5646
|
+
}
|
|
5578
5647
|
restoreReadOnlyFields(payload, orig) {
|
|
5579
5648
|
const editableSections = [
|
|
5580
5649
|
'general',
|
|
@@ -5604,7 +5673,7 @@ class SettingsPageComponent {
|
|
|
5604
5673
|
}
|
|
5605
5674
|
}
|
|
5606
5675
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5607
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: SettingsPageComponent, isStandalone: true, selector: "eds-settings-page", inputs: { presentation: { classPropertyName: "presentation", publicName: "presentation", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { savedSuccessfully: "savedSuccessfully" }, host: { properties: { "class.h-full": "presentation() === \"drawer\"", "class.min-h-0": "presentation() === \"drawer\"", "class.flex": "presentation() === \"drawer\"", "class.flex-col": "presentation() === \"drawer\"", "class.overflow-hidden": "presentation() === \"drawer\"" }, classAttribute: "block" }, ngImport: i0, template: "<div\n class=\"settings-page\"\n [class.p-base]=\"presentation() === 'page'\"\n [class.bg-lighter]=\"presentation() === 'page'\"\n [class.min-h-full]=\"presentation() === 'page'\"\n [class.h-full]=\"presentation() === 'page'\"\n [class.min-h-0]=\"presentation() === 'page'\"\n [class.overflow-auto]=\"presentation() === 'page'\"\n [class.flex]=\"presentation() === 'drawer'\"\n [class.h-full]=\"presentation() === 'drawer'\"\n [class.min-h-0]=\"presentation() === 'drawer'\"\n [class.flex-col]=\"presentation() === 'drawer'\"\n>\n @if (presentation() === 'page') {\n <header class=\"mb-6 flex items-start justify-between gap-4\">\n <div>\n <h4>Agent settings</h4>\n <p class=\"mt-2 text-base leading-normal text-zinc-500\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"flex shrink-0 items-center gap-3\">\n @if (saved()) {\n <span class=\"text-sm font-medium text-green-500\">Agent settings saved</span>\n }\n <ed-button variant=\"primary\" fill size=\"sm\" [disabled]=\"saving()\" (click$)=\"save()\">\n <ed-icon slot=\"prefix\" name=\"save\" [size]=\"16\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </ed-button>\n </div>\n </header>\n }\n\n @if (loading()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\" aria-busy=\"true\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"presentation\">\n @for (_ of navSkeletonRows; track _) {\n <li class=\"mb-1\">\n <ed-skeleton class=\"h-8 w-full !rounded-md\"></ed-skeleton>\n </li>\n }\n </ul>\n </nav>\n <article class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n } @else {\n <div\n class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\"\n aria-busy=\"true\"\n >\n <nav class=\"flex flex-col gap-1 rounded-lg border border-base bg-white p-2 shadow-sm\" aria-label=\"Settings sections\">\n @for (_ of navSkeletonRows; track _) {\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n }\n </nav>\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n }\n } @else if (settings()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"tablist\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <li\n role=\"presentation\"\n [class.cursor-pointer]=\"!isTabDisabled(t.id)\"\n [class.cursor-not-allowed]=\"isTabDisabled(t.id)\"\n >\n <div\n class=\"kpi-menu\"\n role=\"tab\"\n [attr.id]=\"'agent-settings-tab-' + t.id\"\n [attr.aria-selected]=\"tab() === t.id\"\n [attr.aria-disabled]=\"isTabDisabled(t.id)\"\n [attr.tabindex]=\"tab() === t.id ? 0 : -1\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n [class.kpi-menu-active]=\"tab() === t.id\"\n [class.opacity-45]=\"isTabDisabled(t.id)\"\n (click)=\"setTab(t.id)\"\n (keydown.enter)=\"setTab(t.id)\"\n (keydown.space)=\"$event.preventDefault(); setTab(t.id)\"\n >\n <div class=\"flex items-center gap-xs w-full\">\n <span class=\"whitespace-nowrap flex-1 truncate\">{{ t.label }}</span>\n @if (isTabDisabled(t.id)) {\n <span class=\"shrink-0 fs-2xs font-medium uppercase tracking-wide text-lighter\">Off</span>\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </nav>\n\n <article\n class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\"\n >\n <header class=\"flex shrink-0 items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n } @else {\n <div class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\">\n <nav\n class=\"sticky top-4 flex max-h-[calc(100vh-120px)] max-[960px]:static max-[960px]:max-h-none max-[960px]:flex-row max-[960px]:flex-wrap flex-col gap-1 overflow-y-auto rounded-lg border border-base bg-white p-2 shadow-sm\"\n aria-label=\"Settings sections\"\n >\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item flex w-full max-[960px]:w-auto max-[960px]:flex-1 cursor-pointer items-center justify-between gap-3 rounded-md border-none bg-transparent px-4 py-3 text-left text-sm font-medium leading-snug text-zinc-600\"\n [class.settings-page__nav-item--active]=\"tab() === t.id\"\n [class.settings-page__nav-item--disabled]=\"isTabDisabled(t.id)\"\n [disabled]=\"isTabDisabled(t.id)\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n (click)=\"setTab(t.id)\"\n >\n {{ t.label }}\n @if (isTabDisabled(t.id)) {\n <span class=\"ml-auto text-xs font-medium uppercase tracking-wide text-zinc-400\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"flex items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n }\n }\n</div>\n\n<ng-template #settingsPanels>\n @if (tab() === 'knowledge') {\n <div class=\"flex w-full max-w-4xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>What to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span class=\"eds-req\" aria-hidden=\"true\">*</span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"flex flex-col gap-sm\">\n <div class=\"flex flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">Appointment booking mode</span>\n <span class=\"fs-xs text-light\">\n Controls how the voice agent writes bookings to the EHR for this practice.\n Saves immediately \u2014 separate from Save above.\n </span>\n </div>\n\n @if (bookingModeLoading()) {\n <p class=\"m-0 fs-sm text-light\">Loading booking mode\u2026</p>\n } @else {\n <div class=\"flex flex-col gap-xs\" role=\"radiogroup\" aria-label=\"Appointment booking mode\">\n @for (opt of bookingModeOptions(); track opt.value) {\n <button\n type=\"button\"\n class=\"settings-page__booking-option flex w-full cursor-pointer items-start gap-sm rounded-md border border-base bg-white p-sm text-left\"\n [class.settings-page__booking-option--active]=\"bookingMode() === opt.value\"\n [disabled]=\"bookingModeSaving()\"\n role=\"radio\"\n [attr.aria-checked]=\"bookingMode() === opt.value\"\n (click)=\"selectBookingMode(opt.value)\"\n >\n <span\n class=\"settings-page__booking-radio mt-2xs flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-zinc-300\"\n [class.settings-page__booking-radio--on]=\"bookingMode() === opt.value\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">{{ opt.label }}</span>\n @if (opt.description) {\n <span class=\"fs-xs text-light\">{{ opt.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n @if (bookingModeSaving()) {\n <p class=\"m-0 fs-xs text-light\">Saving\u2026</p>\n }\n }\n\n @if (bookingModeError(); as err) {\n <p class=\"m-0 rounded-md border border-red-200 bg-red-50 px-sm py-xs fs-sm text-red-600\" role=\"alert\">\n {{ err }}\n </p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\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=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}:host.h-full{min-height:0}.settings-page__layout{grid-template-columns:240px minmax(0,1fr)}.settings-page__nav-item:hover{background:var(--eds-surface-l1, #f7f9fa);color:var(--eds-text-primary, #41464c)}.settings-page__nav-item--active{background:var(--eds-color-primary-light, #eaf1fe);color:var(--eds-text-brand, #2162f0)}.settings-page__nav-item--disabled,.settings-page__nav-item:disabled{opacity:.45;cursor:not-allowed;color:var(--eds-text-tertiary, #767676)}.settings-page__nav-item--disabled:hover,.settings-page__nav-item:disabled:hover{background:none;color:var(--eds-text-tertiary, #767676)}.settings-page__select{min-height:2.25rem;background:#fff;border:1px solid #c5c7d0;color:#1f2328}.settings-page__locations-menu{box-shadow:0 8px 24px #0f172a29}.settings-page__voice-play{color:var(--eds-text-primary, #41464c)}.settings-page__voice-play:hover:not(:disabled){border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__tone--active{background:var(--eds-surface-l0, #fff);border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:var(--eds-gray-300, #cbd5e1);padding:3px;cursor:pointer;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__booking-option:hover:not(:disabled){border-color:#94c5f9;background:#f8fbff}.settings-page__booking-option--active{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-color-primary-light, #eaf1fe)}.settings-page__booking-option:disabled{opacity:.7;cursor:wait}.settings-page__booking-radio--on{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-brand-600, #2162f0);box-shadow:inset 0 0 0 3px #fff}.eds-req{display:inline;margin-left:.2em;color:#e10d0d;font-size:.85em;font-weight:600;line-height:1;vertical-align:text-top;position:static}.eds-req:after{content:none!important}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ed-icon", inputs: ["name", "size", "color", "cssClass", "decorative", "ariaHidden", "ariaLabel", "title", "desc", "focusable", "tabIndex"] }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }, { kind: "component", type: KnowledgeTestModalComponent, selector: "eds-knowledge-test-modal", inputs: ["open", "whatToSay", "whatNotToSay", "voiceId"], outputs: ["closed"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: EdSkeletonComponent, selector: "ed-skeleton", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5676
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: SettingsPageComponent, isStandalone: true, selector: "eds-settings-page", inputs: { presentation: { classPropertyName: "presentation", publicName: "presentation", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { savedSuccessfully: "savedSuccessfully" }, host: { properties: { "class.settings-page-host--drawer": "presentation() === \"drawer\"" } }, ngImport: i0, template: "<div\n class=\"settings-page\"\n [class.p-base]=\"presentation() === 'page'\"\n [class.bg-lighter]=\"presentation() === 'page'\"\n [class.min-h-full]=\"presentation() === 'page'\"\n [class.h-full]=\"presentation() === 'page'\"\n [class.min-h-0]=\"presentation() === 'page'\"\n [class.overflow-auto]=\"presentation() === 'page'\"\n [class.flex]=\"presentation() === 'drawer'\"\n [class.h-full]=\"presentation() === 'drawer'\"\n [class.min-h-0]=\"presentation() === 'drawer'\"\n [class.flex-col]=\"presentation() === 'drawer'\"\n>\n @if (presentation() === 'page') {\n <header class=\"mb-6 flex items-start justify-between gap-4\">\n <div>\n <h4>Agent settings</h4>\n <p class=\"mt-2 text-base leading-normal text-zinc-500\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"flex shrink-0 items-center gap-3\">\n @if (saved()) {\n <span class=\"text-sm font-medium text-green-500\">Saved</span>\n }\n <ed-button variant=\"primary\" fill size=\"sm\" [disabled]=\"saving()\" (click$)=\"save()\">\n <ed-icon slot=\"prefix\" name=\"save\" [size]=\"16\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </ed-button>\n </div>\n </header>\n }\n\n @if (loading()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\" aria-busy=\"true\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"presentation\">\n @for (_ of navSkeletonRows; track _) {\n <li class=\"mb-1\">\n <ed-skeleton class=\"h-8 w-full !rounded-md\"></ed-skeleton>\n </li>\n }\n </ul>\n </nav>\n <article class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n } @else {\n <div\n class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\"\n aria-busy=\"true\"\n >\n <nav class=\"flex flex-col gap-1 rounded-lg border border-base bg-white p-2 shadow-sm\" aria-label=\"Settings sections\">\n @for (_ of navSkeletonRows; track _) {\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n }\n </nav>\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n }\n } @else if (settings()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"tablist\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <li\n role=\"presentation\"\n [class.cursor-pointer]=\"!isTabDisabled(t.id)\"\n [class.cursor-not-allowed]=\"isTabDisabled(t.id)\"\n >\n <div\n class=\"kpi-menu\"\n role=\"tab\"\n [attr.id]=\"'agent-settings-tab-' + t.id\"\n [attr.aria-selected]=\"tab() === t.id\"\n [attr.aria-disabled]=\"isTabDisabled(t.id)\"\n [attr.tabindex]=\"tab() === t.id ? 0 : -1\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n [class.kpi-menu-active]=\"tab() === t.id\"\n [class.opacity-45]=\"isTabDisabled(t.id)\"\n (click)=\"setTab(t.id)\"\n (keydown.enter)=\"setTab(t.id)\"\n (keydown.space)=\"$event.preventDefault(); setTab(t.id)\"\n >\n <div class=\"flex items-center gap-xs w-full\">\n <span class=\"whitespace-nowrap flex-1 truncate\">{{ t.label }}</span>\n @if (isTabDisabled(t.id)) {\n <span class=\"shrink-0 fs-2xs font-medium uppercase tracking-wide text-lighter\">Off</span>\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </nav>\n\n <article\n class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md bg-white p-base\"\n [class.overflow-y-auto]=\"!locationsMenuOpen()\"\n [class.overflow-visible]=\"locationsMenuOpen()\"\n >\n <header class=\"flex shrink-0 items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n } @else {\n <div class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\">\n <nav\n class=\"sticky top-4 flex max-h-[calc(100vh-120px)] max-[960px]:static max-[960px]:max-h-none max-[960px]:flex-row max-[960px]:flex-wrap flex-col gap-1 overflow-y-auto rounded-lg border border-base bg-white p-2 shadow-sm\"\n aria-label=\"Settings sections\"\n >\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item flex w-full max-[960px]:w-auto max-[960px]:flex-1 cursor-pointer items-center justify-between gap-3 rounded-md border-none bg-transparent px-4 py-3 text-left text-sm font-medium leading-snug text-zinc-600\"\n [class.settings-page__nav-item--active]=\"tab() === t.id\"\n [class.settings-page__nav-item--disabled]=\"isTabDisabled(t.id)\"\n [disabled]=\"isTabDisabled(t.id)\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n (click)=\"setTab(t.id)\"\n >\n {{ t.label }}\n @if (isTabDisabled(t.id)) {\n <span class=\"ml-auto text-xs font-medium uppercase tracking-wide text-zinc-400\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"flex items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n }\n }\n</div>\n\n<ng-template #settingsPanels>\n @if (tab() === 'knowledge') {\n <div class=\"flex w-full max-w-4xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>What to say<span required></span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span required></span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span required></span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span required></span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span required></span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span required></span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span required></span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span required></span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span required></span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"settings-page__booking-mode flex flex-col gap-sm\">\n <div class=\"flex flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">Appointment booking mode</span>\n <span class=\"fs-xs text-light\">\n Controls how the voice agent writes bookings to the EHR for this practice.\n Applied when you click Save changes.\n </span>\n </div>\n\n @if (bookingModeLoading()) {\n <p class=\"m-0 fs-sm text-light\">Loading booking mode\u2026</p>\n } @else {\n <div class=\"flex flex-col gap-xs\" role=\"radiogroup\" aria-label=\"Appointment booking mode\">\n @for (opt of bookingModeOptions(); track opt.value) {\n <button\n type=\"button\"\n class=\"settings-page__booking-option flex w-full cursor-pointer items-start gap-sm rounded-md border border-base bg-white p-sm text-left\"\n [class.settings-page__booking-option--active]=\"bookingMode() === opt.value\"\n [disabled]=\"saving()\"\n role=\"radio\"\n [attr.aria-checked]=\"bookingMode() === opt.value\"\n (click)=\"selectBookingMode(opt.value)\"\n >\n <span\n class=\"settings-page__booking-radio mt-2xs flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-zinc-300\"\n [class.settings-page__booking-radio--on]=\"bookingMode() === opt.value\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">{{ opt.label }}</span>\n @if (opt.description) {\n <span class=\"fs-xs text-light\">{{ opt.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n\n @if (bookingModeError(); as err) {\n <p class=\"m-0 rounded-md border border-red-200 bg-red-50 px-sm py-xs fs-sm text-red-600\" role=\"alert\">\n {{ err }}\n </p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\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=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}:host.settings-page-host--drawer{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.settings-page__layout{grid-template-columns:240px minmax(0,1fr)}.settings-page__nav-item:hover{background:var(--eds-surface-l1, #f7f9fa);color:var(--eds-text-primary, #41464c)}.settings-page__nav-item--active{background:var(--eds-color-primary-light, #eaf1fe);color:var(--eds-text-brand, #2162f0)}.settings-page__nav-item--disabled,.settings-page__nav-item:disabled{opacity:.45;cursor:not-allowed;color:var(--eds-text-tertiary, #767676)}.settings-page__nav-item--disabled:hover,.settings-page__nav-item:disabled:hover{background:none;color:var(--eds-text-tertiary, #767676)}.settings-page__select{min-height:2.25rem;background:#fff;border:1px solid #c5c7d0;color:#1f2328}.settings-page__locations-menu{box-shadow:0 8px 24px #0f172a29}.settings-page__voice-play{color:var(--eds-text-primary, #41464c)}.settings-page__voice-play:hover:not(:disabled){border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__tone--active{background:var(--eds-surface-l0, #fff);border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:var(--eds-gray-300, #cbd5e1);padding:3px;cursor:pointer;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__booking-option:hover:not(:disabled){border-color:#94c5f9;background:#f8fbff}.settings-page__booking-option--active{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-color-primary-light, #eaf1fe)}.settings-page__booking-option:disabled{opacity:.7;cursor:wait}.settings-page__booking-radio--on{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-brand-600, #2162f0);box-shadow:inset 0 0 0 3px #fff}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ed-icon", inputs: ["name", "size", "color", "cssClass", "decorative", "ariaHidden", "ariaLabel", "title", "desc", "focusable", "tabIndex"] }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }, { kind: "component", type: KnowledgeTestModalComponent, selector: "eds-knowledge-test-modal", inputs: ["open", "whatToSay", "whatNotToSay", "voiceId"], outputs: ["closed"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: EdSkeletonComponent, selector: "ed-skeleton", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5608
5677
|
}
|
|
5609
5678
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, decorators: [{
|
|
5610
5679
|
type: Component,
|
|
@@ -5617,13 +5686,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
5617
5686
|
ModalComponent,
|
|
5618
5687
|
EdSkeletonComponent,
|
|
5619
5688
|
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
5620
|
-
class: '
|
|
5621
|
-
'[class.h-full]': 'presentation() === "drawer"',
|
|
5622
|
-
'[class.min-h-0]': 'presentation() === "drawer"',
|
|
5623
|
-
'[class.flex]': 'presentation() === "drawer"',
|
|
5624
|
-
'[class.flex-col]': 'presentation() === "drawer"',
|
|
5625
|
-
'[class.overflow-hidden]': 'presentation() === "drawer"',
|
|
5626
|
-
}, template: "<div\n class=\"settings-page\"\n [class.p-base]=\"presentation() === 'page'\"\n [class.bg-lighter]=\"presentation() === 'page'\"\n [class.min-h-full]=\"presentation() === 'page'\"\n [class.h-full]=\"presentation() === 'page'\"\n [class.min-h-0]=\"presentation() === 'page'\"\n [class.overflow-auto]=\"presentation() === 'page'\"\n [class.flex]=\"presentation() === 'drawer'\"\n [class.h-full]=\"presentation() === 'drawer'\"\n [class.min-h-0]=\"presentation() === 'drawer'\"\n [class.flex-col]=\"presentation() === 'drawer'\"\n>\n @if (presentation() === 'page') {\n <header class=\"mb-6 flex items-start justify-between gap-4\">\n <div>\n <h4>Agent settings</h4>\n <p class=\"mt-2 text-base leading-normal text-zinc-500\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"flex shrink-0 items-center gap-3\">\n @if (saved()) {\n <span class=\"text-sm font-medium text-green-500\">Agent settings saved</span>\n }\n <ed-button variant=\"primary\" fill size=\"sm\" [disabled]=\"saving()\" (click$)=\"save()\">\n <ed-icon slot=\"prefix\" name=\"save\" [size]=\"16\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </ed-button>\n </div>\n </header>\n }\n\n @if (loading()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\" aria-busy=\"true\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"presentation\">\n @for (_ of navSkeletonRows; track _) {\n <li class=\"mb-1\">\n <ed-skeleton class=\"h-8 w-full !rounded-md\"></ed-skeleton>\n </li>\n }\n </ul>\n </nav>\n <article class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n } @else {\n <div\n class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\"\n aria-busy=\"true\"\n >\n <nav class=\"flex flex-col gap-1 rounded-lg border border-base bg-white p-2 shadow-sm\" aria-label=\"Settings sections\">\n @for (_ of navSkeletonRows; track _) {\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n }\n </nav>\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n }\n } @else if (settings()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"tablist\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <li\n role=\"presentation\"\n [class.cursor-pointer]=\"!isTabDisabled(t.id)\"\n [class.cursor-not-allowed]=\"isTabDisabled(t.id)\"\n >\n <div\n class=\"kpi-menu\"\n role=\"tab\"\n [attr.id]=\"'agent-settings-tab-' + t.id\"\n [attr.aria-selected]=\"tab() === t.id\"\n [attr.aria-disabled]=\"isTabDisabled(t.id)\"\n [attr.tabindex]=\"tab() === t.id ? 0 : -1\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n [class.kpi-menu-active]=\"tab() === t.id\"\n [class.opacity-45]=\"isTabDisabled(t.id)\"\n (click)=\"setTab(t.id)\"\n (keydown.enter)=\"setTab(t.id)\"\n (keydown.space)=\"$event.preventDefault(); setTab(t.id)\"\n >\n <div class=\"flex items-center gap-xs w-full\">\n <span class=\"whitespace-nowrap flex-1 truncate\">{{ t.label }}</span>\n @if (isTabDisabled(t.id)) {\n <span class=\"shrink-0 fs-2xs font-medium uppercase tracking-wide text-lighter\">Off</span>\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </nav>\n\n <article\n class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\"\n >\n <header class=\"flex shrink-0 items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n } @else {\n <div class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\">\n <nav\n class=\"sticky top-4 flex max-h-[calc(100vh-120px)] max-[960px]:static max-[960px]:max-h-none max-[960px]:flex-row max-[960px]:flex-wrap flex-col gap-1 overflow-y-auto rounded-lg border border-base bg-white p-2 shadow-sm\"\n aria-label=\"Settings sections\"\n >\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item flex w-full max-[960px]:w-auto max-[960px]:flex-1 cursor-pointer items-center justify-between gap-3 rounded-md border-none bg-transparent px-4 py-3 text-left text-sm font-medium leading-snug text-zinc-600\"\n [class.settings-page__nav-item--active]=\"tab() === t.id\"\n [class.settings-page__nav-item--disabled]=\"isTabDisabled(t.id)\"\n [disabled]=\"isTabDisabled(t.id)\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n (click)=\"setTab(t.id)\"\n >\n {{ t.label }}\n @if (isTabDisabled(t.id)) {\n <span class=\"ml-auto text-xs font-medium uppercase tracking-wide text-zinc-400\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"flex items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n }\n }\n</div>\n\n<ng-template #settingsPanels>\n @if (tab() === 'knowledge') {\n <div class=\"flex w-full max-w-4xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>What to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span class=\"eds-req\" aria-hidden=\"true\">*</span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"flex flex-col gap-sm\">\n <div class=\"flex flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">Appointment booking mode</span>\n <span class=\"fs-xs text-light\">\n Controls how the voice agent writes bookings to the EHR for this practice.\n Saves immediately \u2014 separate from Save above.\n </span>\n </div>\n\n @if (bookingModeLoading()) {\n <p class=\"m-0 fs-sm text-light\">Loading booking mode\u2026</p>\n } @else {\n <div class=\"flex flex-col gap-xs\" role=\"radiogroup\" aria-label=\"Appointment booking mode\">\n @for (opt of bookingModeOptions(); track opt.value) {\n <button\n type=\"button\"\n class=\"settings-page__booking-option flex w-full cursor-pointer items-start gap-sm rounded-md border border-base bg-white p-sm text-left\"\n [class.settings-page__booking-option--active]=\"bookingMode() === opt.value\"\n [disabled]=\"bookingModeSaving()\"\n role=\"radio\"\n [attr.aria-checked]=\"bookingMode() === opt.value\"\n (click)=\"selectBookingMode(opt.value)\"\n >\n <span\n class=\"settings-page__booking-radio mt-2xs flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-zinc-300\"\n [class.settings-page__booking-radio--on]=\"bookingMode() === opt.value\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">{{ opt.label }}</span>\n @if (opt.description) {\n <span class=\"fs-xs text-light\">{{ opt.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n @if (bookingModeSaving()) {\n <p class=\"m-0 fs-xs text-light\">Saving\u2026</p>\n }\n }\n\n @if (bookingModeError(); as err) {\n <p class=\"m-0 rounded-md border border-red-200 bg-red-50 px-sm py-xs fs-sm text-red-600\" role=\"alert\">\n {{ err }}\n </p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\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=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}:host.h-full{min-height:0}.settings-page__layout{grid-template-columns:240px minmax(0,1fr)}.settings-page__nav-item:hover{background:var(--eds-surface-l1, #f7f9fa);color:var(--eds-text-primary, #41464c)}.settings-page__nav-item--active{background:var(--eds-color-primary-light, #eaf1fe);color:var(--eds-text-brand, #2162f0)}.settings-page__nav-item--disabled,.settings-page__nav-item:disabled{opacity:.45;cursor:not-allowed;color:var(--eds-text-tertiary, #767676)}.settings-page__nav-item--disabled:hover,.settings-page__nav-item:disabled:hover{background:none;color:var(--eds-text-tertiary, #767676)}.settings-page__select{min-height:2.25rem;background:#fff;border:1px solid #c5c7d0;color:#1f2328}.settings-page__locations-menu{box-shadow:0 8px 24px #0f172a29}.settings-page__voice-play{color:var(--eds-text-primary, #41464c)}.settings-page__voice-play:hover:not(:disabled){border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__tone--active{background:var(--eds-surface-l0, #fff);border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:var(--eds-gray-300, #cbd5e1);padding:3px;cursor:pointer;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__booking-option:hover:not(:disabled){border-color:#94c5f9;background:#f8fbff}.settings-page__booking-option--active{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-color-primary-light, #eaf1fe)}.settings-page__booking-option:disabled{opacity:.7;cursor:wait}.settings-page__booking-radio--on{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-brand-600, #2162f0);box-shadow:inset 0 0 0 3px #fff}.eds-req{display:inline;margin-left:.2em;color:#e10d0d;font-size:.85em;font-weight:600;line-height:1;vertical-align:text-top;position:static}.eds-req:after{content:none!important}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"] }]
|
|
5689
|
+
'[class.settings-page-host--drawer]': 'presentation() === "drawer"',
|
|
5690
|
+
}, template: "<div\n class=\"settings-page\"\n [class.p-base]=\"presentation() === 'page'\"\n [class.bg-lighter]=\"presentation() === 'page'\"\n [class.min-h-full]=\"presentation() === 'page'\"\n [class.h-full]=\"presentation() === 'page'\"\n [class.min-h-0]=\"presentation() === 'page'\"\n [class.overflow-auto]=\"presentation() === 'page'\"\n [class.flex]=\"presentation() === 'drawer'\"\n [class.h-full]=\"presentation() === 'drawer'\"\n [class.min-h-0]=\"presentation() === 'drawer'\"\n [class.flex-col]=\"presentation() === 'drawer'\"\n>\n @if (presentation() === 'page') {\n <header class=\"mb-6 flex items-start justify-between gap-4\">\n <div>\n <h4>Agent settings</h4>\n <p class=\"mt-2 text-base leading-normal text-zinc-500\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"flex shrink-0 items-center gap-3\">\n @if (saved()) {\n <span class=\"text-sm font-medium text-green-500\">Saved</span>\n }\n <ed-button variant=\"primary\" fill size=\"sm\" [disabled]=\"saving()\" (click$)=\"save()\">\n <ed-icon slot=\"prefix\" name=\"save\" [size]=\"16\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </ed-button>\n </div>\n </header>\n }\n\n @if (loading()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\" aria-busy=\"true\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"presentation\">\n @for (_ of navSkeletonRows; track _) {\n <li class=\"mb-1\">\n <ed-skeleton class=\"h-8 w-full !rounded-md\"></ed-skeleton>\n </li>\n }\n </ul>\n </nav>\n <article class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md overflow-y-auto bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n } @else {\n <div\n class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\"\n aria-busy=\"true\"\n >\n <nav class=\"flex flex-col gap-1 rounded-lg border border-base bg-white p-2 shadow-sm\" aria-label=\"Settings sections\">\n @for (_ of navSkeletonRows; track _) {\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n }\n </nav>\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-5 w-40\"></ed-skeleton>\n <ed-skeleton class=\"h-3 w-72\"></ed-skeleton>\n </header>\n @for (_ of fieldSkeletonRows; track _) {\n <div class=\"grid gap-2xs\">\n <ed-skeleton class=\"h-3 w-24\"></ed-skeleton>\n <ed-skeleton class=\"h-10 w-full !rounded-md\"></ed-skeleton>\n </div>\n }\n </article>\n </div>\n }\n } @else if (settings()) {\n @if (presentation() === 'drawer') {\n <div class=\"flex min-h-0 flex-1\">\n <nav class=\"w-[220px] h-full shrink-0 overflow-y-auto p-base bg-lighter-2\" aria-label=\"Settings sections\">\n <ul role=\"tablist\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <li\n role=\"presentation\"\n [class.cursor-pointer]=\"!isTabDisabled(t.id)\"\n [class.cursor-not-allowed]=\"isTabDisabled(t.id)\"\n >\n <div\n class=\"kpi-menu\"\n role=\"tab\"\n [attr.id]=\"'agent-settings-tab-' + t.id\"\n [attr.aria-selected]=\"tab() === t.id\"\n [attr.aria-disabled]=\"isTabDisabled(t.id)\"\n [attr.tabindex]=\"tab() === t.id ? 0 : -1\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n [class.kpi-menu-active]=\"tab() === t.id\"\n [class.opacity-45]=\"isTabDisabled(t.id)\"\n (click)=\"setTab(t.id)\"\n (keydown.enter)=\"setTab(t.id)\"\n (keydown.space)=\"$event.preventDefault(); setTab(t.id)\"\n >\n <div class=\"flex items-center gap-xs w-full\">\n <span class=\"whitespace-nowrap flex-1 truncate\">{{ t.label }}</span>\n @if (isTabDisabled(t.id)) {\n <span class=\"shrink-0 fs-2xs font-medium uppercase tracking-wide text-lighter\">Off</span>\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </nav>\n\n <article\n class=\"flex min-h-0 min-w-0 flex-1 flex-col gap-md bg-white p-base\"\n [class.overflow-y-auto]=\"!locationsMenuOpen()\"\n [class.overflow-visible]=\"locationsMenuOpen()\"\n >\n <header class=\"flex shrink-0 items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n } @else {\n <div class=\"grid grid-cols-[240px_minmax(0,1fr)] max-[960px]:grid-cols-1 items-start gap-5\">\n <nav\n class=\"sticky top-4 flex max-h-[calc(100vh-120px)] max-[960px]:static max-[960px]:max-h-none max-[960px]:flex-row max-[960px]:flex-wrap flex-col gap-1 overflow-y-auto rounded-lg border border-base bg-white p-2 shadow-sm\"\n aria-label=\"Settings sections\"\n >\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item flex w-full max-[960px]:w-auto max-[960px]:flex-1 cursor-pointer items-center justify-between gap-3 rounded-md border-none bg-transparent px-4 py-3 text-left text-sm font-medium leading-snug text-zinc-600\"\n [class.settings-page__nav-item--active]=\"tab() === t.id\"\n [class.settings-page__nav-item--disabled]=\"isTabDisabled(t.id)\"\n [disabled]=\"isTabDisabled(t.id)\"\n [attr.title]=\"isTabDisabled(t.id) ? 'This section is currently disabled' : null\"\n (click)=\"setTab(t.id)\"\n >\n {{ t.label }}\n @if (isTabDisabled(t.id)) {\n <span class=\"ml-auto text-xs font-medium uppercase tracking-wide text-zinc-400\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"flex min-h-96 flex-col gap-md overflow-hidden rounded-lg border border-light bg-white p-base\">\n <header class=\"flex items-start justify-between gap-base\">\n <div class=\"min-w-0\">\n <h5 class=\"m-0\">{{ sectionMeta().title }}</h5>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"m-0 mt-2xs max-w-2xl fs-sm text-light\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"openTestCall()\">\n <ed-icon slot=\"prefix\" name=\"phone_call\" [size]=\"16\" />\n Test Call\n </ed-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"flex items-center gap-sm rounded-md border border-light bg-lighter px-base py-sm fs-sm text-light\">\n <ed-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n <ng-container [ngTemplateOutlet]=\"settingsPanels\" />\n </article>\n </div>\n }\n }\n</div>\n\n<ng-template #settingsPanels>\n @if (tab() === 'knowledge') {\n <div class=\"flex w-full max-w-4xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>What to say<span required></span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span required></span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span required></span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span required></span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span required></span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span required></span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span required></span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span required></span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span required></span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"settings-page__booking-mode flex flex-col gap-sm\">\n <div class=\"flex flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">Appointment booking mode</span>\n <span class=\"fs-xs text-light\">\n Controls how the voice agent writes bookings to the EHR for this practice.\n Applied when you click Save changes.\n </span>\n </div>\n\n @if (bookingModeLoading()) {\n <p class=\"m-0 fs-sm text-light\">Loading booking mode\u2026</p>\n } @else {\n <div class=\"flex flex-col gap-xs\" role=\"radiogroup\" aria-label=\"Appointment booking mode\">\n @for (opt of bookingModeOptions(); track opt.value) {\n <button\n type=\"button\"\n class=\"settings-page__booking-option flex w-full cursor-pointer items-start gap-sm rounded-md border border-base bg-white p-sm text-left\"\n [class.settings-page__booking-option--active]=\"bookingMode() === opt.value\"\n [disabled]=\"saving()\"\n role=\"radio\"\n [attr.aria-checked]=\"bookingMode() === opt.value\"\n (click)=\"selectBookingMode(opt.value)\"\n >\n <span\n class=\"settings-page__booking-radio mt-2xs flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-zinc-300\"\n [class.settings-page__booking-radio--on]=\"bookingMode() === opt.value\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm font-medium text-dark\">{{ opt.label }}</span>\n @if (opt.description) {\n <span class=\"fs-xs text-light\">{{ opt.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n\n @if (bookingModeError(); as err) {\n <p class=\"m-0 rounded-md border border-red-200 bg-red-50 px-sm py-xs fs-sm text-red-600\" role=\"alert\">\n {{ err }}\n </p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span required></span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\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=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}:host.settings-page-host--drawer{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.settings-page__layout{grid-template-columns:240px minmax(0,1fr)}.settings-page__nav-item:hover{background:var(--eds-surface-l1, #f7f9fa);color:var(--eds-text-primary, #41464c)}.settings-page__nav-item--active{background:var(--eds-color-primary-light, #eaf1fe);color:var(--eds-text-brand, #2162f0)}.settings-page__nav-item--disabled,.settings-page__nav-item:disabled{opacity:.45;cursor:not-allowed;color:var(--eds-text-tertiary, #767676)}.settings-page__nav-item--disabled:hover,.settings-page__nav-item:disabled:hover{background:none;color:var(--eds-text-tertiary, #767676)}.settings-page__select{min-height:2.25rem;background:#fff;border:1px solid #c5c7d0;color:#1f2328}.settings-page__locations-menu{box-shadow:0 8px 24px #0f172a29}.settings-page__voice-play{color:var(--eds-text-primary, #41464c)}.settings-page__voice-play:hover:not(:disabled){border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__tone--active{background:var(--eds-surface-l0, #fff);border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:var(--eds-gray-300, #cbd5e1);padding:3px;cursor:pointer;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__booking-option:hover:not(:disabled){border-color:#94c5f9;background:#f8fbff}.settings-page__booking-option--active{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-color-primary-light, #eaf1fe)}.settings-page__booking-option:disabled{opacity:.7;cursor:wait}.settings-page__booking-radio--on{border-color:var(--eds-brand-600, #2162f0);background:var(--eds-brand-600, #2162f0);box-shadow:inset 0 0 0 3px #fff}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"] }]
|
|
5627
5691
|
}] });
|
|
5628
5692
|
|
|
5629
5693
|
var settingsPage_component = /*#__PURE__*/Object.freeze({
|
|
@@ -5894,16 +5958,13 @@ class AgentSettingsDrawerComponent {
|
|
|
5894
5958
|
this.settingsForm()?.discardChanges();
|
|
5895
5959
|
this.drawer.hide();
|
|
5896
5960
|
}
|
|
5897
|
-
onSaved() {
|
|
5898
|
-
this.drawer.hide();
|
|
5899
|
-
}
|
|
5900
5961
|
onBackdropClick(event) {
|
|
5901
5962
|
if (event.target === event.currentTarget) {
|
|
5902
5963
|
this.close();
|
|
5903
5964
|
}
|
|
5904
5965
|
}
|
|
5905
5966
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentSettingsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5906
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: AgentSettingsDrawerComponent, isStandalone: true, selector: "eds-agent-settings-drawer", viewQueries: [{ propertyName: "settingsForm", first: true, predicate: ["settingsForm"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (open()) {\n <div\n edsViewportPortal\n class=\"eds-drawer-backdrop fixed left-0 top-0 z-[100100] flex h-[100vh] w-[100vw] justify-end\"\n (click)=\"onBackdropClick($event)\"\n role=\"presentation\"\n >\n <ed-drawer\n size=\"xl\"\n class=\"slide-in-rtl ed-main-sidepanel h-full\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Agent Setting\"\n >\n <ed-drawer-header class=\"shrink-0 border-b border-zinc-200 bg-white shadow-none\">\n <h6 data-new-heading=\"true\">Agent Setting</h6>\n <ed-icon-button\n name=\"close\"\n label=\"Close\"\n size=\"sm\"\n ghost\n (click$)=\"close()\"\n />\n </ed-drawer-header>\n\n <ed-drawer-body class=\"
|
|
5967
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: AgentSettingsDrawerComponent, isStandalone: true, selector: "eds-agent-settings-drawer", viewQueries: [{ propertyName: "settingsForm", first: true, predicate: ["settingsForm"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (open()) {\n <div\n edsViewportPortal\n class=\"eds-drawer-backdrop fixed left-0 top-0 z-[100100] flex h-[100vh] w-[100vw] justify-end\"\n (click)=\"onBackdropClick($event)\"\n role=\"presentation\"\n >\n <ed-drawer\n size=\"xl\"\n class=\"slide-in-rtl ed-main-sidepanel h-full\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Agent Setting\"\n >\n <ed-drawer-header class=\"shrink-0 border-b border-zinc-200 bg-white shadow-none\">\n <h6 data-new-heading=\"true\">Agent Setting</h6>\n <ed-icon-button\n name=\"close\"\n label=\"Close\"\n size=\"sm\"\n ghost\n (click$)=\"close()\"\n />\n </ed-drawer-header>\n\n <ed-drawer-body class=\"min-h-0 flex-1 overflow-hidden p-0\">\n <eds-settings-page\n #settingsForm\n presentation=\"drawer\"\n />\n </ed-drawer-body>\n\n <ed-drawer-footer class=\"shrink-0 flex-row-reverse bg-lighter\">\n <ed-button\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"settingsForm.saving()\"\n (click$)=\"settingsForm.save()\"\n >\n {{ settingsForm.saving() ? 'Saving\u2026' : 'Save' }}\n </ed-button>\n <ed-button\n variant=\"default\"\n outline\n size=\"sm\"\n (click$)=\"close()\"\n >\n Cancel\n </ed-button>\n @if (settingsForm.saved()) {\n <span class=\"mr-auto fs-sm font-medium text-green-500\">Saved</span>\n }\n </ed-drawer-footer>\n </ed-drawer>\n </div>\n}\n", styles: ["@charset \"UTF-8\";.eds-drawer-backdrop{background-color:#0f172a66}\n"], dependencies: [{ kind: "component", type: SettingsPageComponent, selector: "eds-settings-page", inputs: ["presentation"], outputs: ["savedSuccessfully"] }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }, { kind: "component", type: EdIconButtonComponent, selector: "ed-icon-button", inputs: ["label", "name", "size", "variant", "allowBubble", "ghost", "outline", "solid", "subtle", "shape", "type", "disabled", "loading", "toggle", "pressed", "id", "title", "nameAttr", "value", "autofocus", "tabIndex"], outputs: ["click", "focus", "blur", "click$", "focus$", "blur$"] }, { kind: "directive", type: ViewportPortalDirective, selector: "[edsViewportPortal]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5907
5968
|
}
|
|
5908
5969
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentSettingsDrawerComponent, decorators: [{
|
|
5909
5970
|
type: Component,
|
|
@@ -5912,7 +5973,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
5912
5973
|
EdButtonComponent,
|
|
5913
5974
|
EdIconButtonComponent,
|
|
5914
5975
|
ViewportPortalDirective,
|
|
5915
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "@if (open()) {\n <div\n edsViewportPortal\n class=\"eds-drawer-backdrop fixed left-0 top-0 z-[100100] flex h-[100vh] w-[100vw] justify-end\"\n (click)=\"onBackdropClick($event)\"\n role=\"presentation\"\n >\n <ed-drawer\n size=\"xl\"\n class=\"slide-in-rtl ed-main-sidepanel h-full\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Agent Setting\"\n >\n <ed-drawer-header class=\"shrink-0 border-b border-zinc-200 bg-white shadow-none\">\n <h6 data-new-heading=\"true\">Agent Setting</h6>\n <ed-icon-button\n name=\"close\"\n label=\"Close\"\n size=\"sm\"\n ghost\n (click$)=\"close()\"\n />\n </ed-drawer-header>\n\n <ed-drawer-body class=\"
|
|
5976
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "@if (open()) {\n <div\n edsViewportPortal\n class=\"eds-drawer-backdrop fixed left-0 top-0 z-[100100] flex h-[100vh] w-[100vw] justify-end\"\n (click)=\"onBackdropClick($event)\"\n role=\"presentation\"\n >\n <ed-drawer\n size=\"xl\"\n class=\"slide-in-rtl ed-main-sidepanel h-full\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Agent Setting\"\n >\n <ed-drawer-header class=\"shrink-0 border-b border-zinc-200 bg-white shadow-none\">\n <h6 data-new-heading=\"true\">Agent Setting</h6>\n <ed-icon-button\n name=\"close\"\n label=\"Close\"\n size=\"sm\"\n ghost\n (click$)=\"close()\"\n />\n </ed-drawer-header>\n\n <ed-drawer-body class=\"min-h-0 flex-1 overflow-hidden p-0\">\n <eds-settings-page\n #settingsForm\n presentation=\"drawer\"\n />\n </ed-drawer-body>\n\n <ed-drawer-footer class=\"shrink-0 flex-row-reverse bg-lighter\">\n <ed-button\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"settingsForm.saving()\"\n (click$)=\"settingsForm.save()\"\n >\n {{ settingsForm.saving() ? 'Saving\u2026' : 'Save' }}\n </ed-button>\n <ed-button\n variant=\"default\"\n outline\n size=\"sm\"\n (click$)=\"close()\"\n >\n Cancel\n </ed-button>\n @if (settingsForm.saved()) {\n <span class=\"mr-auto fs-sm font-medium text-green-500\">Saved</span>\n }\n </ed-drawer-footer>\n </ed-drawer>\n </div>\n}\n", styles: ["@charset \"UTF-8\";.eds-drawer-backdrop{background-color:#0f172a66}\n"] }]
|
|
5916
5977
|
}] });
|
|
5917
5978
|
|
|
5918
5979
|
class EdsModuleNavComponent {
|
|
@@ -5929,10 +5990,21 @@ class EdsModuleNavComponent {
|
|
|
5929
5990
|
this.online = this.agentOnline.online;
|
|
5930
5991
|
this.agentLoading = this.agentOnline.loading;
|
|
5931
5992
|
this.toggling = this.agentOnline.toggling;
|
|
5932
|
-
/**
|
|
5933
|
-
this.
|
|
5934
|
-
|
|
5935
|
-
|
|
5993
|
+
/** Menu open or agent live → Figma Selected / Call answering solid chip. */
|
|
5994
|
+
this.brandIconActive = computed(() => this.headerMenuOpen() || this.online() === true);
|
|
5995
|
+
/** Hover tracked so ed-icon [color] can switch (SVG is outside emulated encapsulation). */
|
|
5996
|
+
this.brandIconHovered = signal(false);
|
|
5997
|
+
/** Explicit fill — currentColor does not reliably tint injected SVG paths. */
|
|
5998
|
+
this.brandIconColor = computed(() => this.brandIconActive() || this.brandIconHovered() ? '#ffffff' : '#9810fa');
|
|
5999
|
+
this.headerAriaLabel = computed(() => {
|
|
6000
|
+
if (this.online() === true)
|
|
6001
|
+
return 'AI Receptionist menu, live';
|
|
6002
|
+
if (this.online() === false)
|
|
6003
|
+
return 'AI Receptionist menu, offline';
|
|
6004
|
+
if (this.agentLoading())
|
|
6005
|
+
return 'AI Receptionist menu, checking status';
|
|
6006
|
+
return 'AI Receptionist menu';
|
|
6007
|
+
});
|
|
5936
6008
|
this.confirmOffOpen = signal(false);
|
|
5937
6009
|
this.headerMenuOpen = signal(false);
|
|
5938
6010
|
this.agentSwitchDisabled = computed(() => this.agentLoading() || this.toggling() || this.online() === null);
|
|
@@ -6005,7 +6077,7 @@ class EdsModuleNavComponent {
|
|
|
6005
6077
|
});
|
|
6006
6078
|
}
|
|
6007
6079
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6008
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsModuleNavComponent, isStandalone: true, selector: "eds-module-nav", host: { classAttribute: "block shrink-0" }, ngImport: i0, template: "<nav\n class=\"sticky top-0 flex h-screen min-h-screen w-52 flex-col overflow-y-auto border-r border-base bg-white \"\n aria-label=\"AI Receptionist\"\n>\n <div class=\"flex min-h-0 flex-1 flex-col \">\n <div\n class=\"relative shrink-0 p-sm\"\n data-header-menu\n (document:click)=\"onHeaderMenuDocClick($event)\"\n >\n <button\n type=\"button\"\n class=\"flex w-full cursor-pointer items-center justify-between gap-xs rounded-md border-0 bg-transparent p-2xs text-left
|
|
6080
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: EdsModuleNavComponent, isStandalone: true, selector: "eds-module-nav", host: { classAttribute: "block shrink-0" }, ngImport: i0, template: "<nav\n class=\"sticky top-0 flex h-screen min-h-screen w-52 flex-col overflow-y-auto border-r border-base bg-white \"\n aria-label=\"AI Receptionist\"\n>\n <div class=\"flex min-h-0 flex-1 flex-col \">\n <div\n class=\"relative shrink-0 p-sm\"\n data-header-menu\n (document:click)=\"onHeaderMenuDocClick($event)\"\n >\n <button\n type=\"button\"\n class=\"ai-receptionist-nav-item flex w-full cursor-pointer items-center justify-between gap-xs rounded-md border-0 bg-transparent p-2xs text-left\"\n [class.active]=\"brandIconActive()\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-expanded]=\"headerMenuOpen()\"\n aria-haspopup=\"menu\"\n (mouseenter)=\"brandIconHovered.set(true)\"\n (mouseleave)=\"brandIconHovered.set(false)\"\n (click)=\"$event.stopPropagation(); toggleHeaderMenu()\"\n >\n <div class=\"flex min-w-0 flex-1 items-center gap-xs\">\n <span\n class=\"menu-item-icon relative flex h-9 w-9 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n <ed-icon\n name=\"ai_headset\"\n [size]=\"20\"\n [color]=\"brandIconColor()\"\n />\n @if (online() === true) {\n <span class=\"ai-receptionist-live-dot\"></span>\n } @else if (online() === false) {\n <span class=\"ai-receptionist-live-dot is-offline\"></span>\n }\n </span>\n <span class=\"min-w-0 truncate fs-base font-semibold text-dark\">AI Receptionist</span>\n </div>\n <ed-icon\n name=\"caret_down\"\n [size]=\"16\"\n color=\"currentColor\"\n class=\"shrink-0 text-light\"\n aria-hidden=\"true\"\n />\n </button>\n @if (headerMenuOpen()) {\n <div\n class=\"fade-in absolute right-0 top-full z-20 mt-2xs flex w-44 origin-top flex-col gap-2xs rounded-md border border-light bg-white p-2xs shadow-md\"\n role=\"menu\"\n aria-label=\"AI Receptionist menu\"\n >\n <a\n class=\"flex w-full items-center rounded-sm px-sm py-xs fs-sm text-dark no-underline transition-colors hover:bg-lighter\"\n routerLink=\"audit\"\n routerLinkActive=\"bg-lighter\"\n role=\"menuitem\"\n (click)=\"closeHeaderMenu()\"\n >\n Audit Logs\n </a>\n <button\n type=\"button\"\n class=\"flex w-full cursor-pointer items-center rounded-sm border-0 bg-transparent px-sm py-xs text-left fs-sm text-dark transition-colors hover:bg-lighter\"\n role=\"menuitem\"\n (click)=\"openSettingsDrawer()\"\n >\n Settings\n </button>\n <div class=\"w-full border-b border-light\" role=\"presentation\"></div>\n <div class=\"flex w-full items-center gap-2xs px-sm py-xs\" role=\"menuitem\">\n <span class=\"min-w-0 flex-1 fs-sm text-dark\">\n @if (online() === true) {\n Agent is Live\n } @else if (online() === false) {\n Agent is Offline\n } @else {\n Agent status\n }\n </span>\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent p-2xs transition-colors disabled:cursor-not-allowed disabled:opacity-55\"\n [class.bg-primary]=\"online() === true\"\n [class.justify-end]=\"online() === true\"\n [class.bg-lighter]=\"online() !== true\"\n [class.justify-start]=\"online() !== true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n [attr.aria-label]=\"online() === true ? 'Turn off AI receptionist' : 'Turn on AI receptionist'\"\n (click)=\"$event.stopPropagation(); onAgentToggleClick()\"\n >\n <span class=\"block h-3 w-3 rounded-full bg-white\"></span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"w-full border-b border-light\" role=\"presentation\"></div>\n\n <ul role=\"menu\" class=\"m-0 list-none p-0\">\n @for (item of receptionistNav; track item.id) {\n <li\n tabindex=\"0\"\n role=\"menuitem\"\n class=\"cursor-pointer px-base py-xs text-left fs-base leading-[1.8] hover:bg-lighter active:bg-base\"\n [attr.data-value]=\"item.label\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"bg-light\"\n [routerLinkActiveOptions]=\"{ exact: item.route === 'dashboard' }\"\n (keydown.enter)=\"$any($event.target)?.click()\"\n >{{ item.label }}</li>\n }\n </ul>\n\n @if (includePracticeNav) {\n <p class=\"mb-0 mt-0 fs-sm font-semibold text-light\">Practice</p>\n <ul role=\"menu\" class=\"m-0 list-none p-0\">\n @for (item of practiceNav; track item.id) {\n @if (showNavItem(item)) {\n <li\n tabindex=\"0\"\n role=\"menuitem\"\n class=\"cursor-pointer px-base py-xs text-left fs-base leading-[1.8] hover:bg-lighter active:bg-base\"\n [attr.data-value]=\"item.label\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"bg-light\"\n (keydown.enter)=\"$any($event.target)?.click()\"\n >{{ item.label }}</li>\n }\n }\n </ul>\n }\n </div>\n\n @if (showLogout()) {\n <div class=\"mt-auto border-t border-light pt-sm\">\n @if (userEmail(); as email) {\n <p class=\"mb-xs truncate px-xs fs-xs text-light\" [title]=\"email\">{{ email }}</p>\n }\n <button\n type=\"button\"\n class=\"group/link relative flex h-10 w-full cursor-pointer items-center gap-xs rounded-md border-0 bg-transparent px-xs text-left fs-base font-medium text-light transition-colors hover:bg-lighter hover:text-dark\"\n (click)=\"logout()\"\n aria-label=\"Log out\"\n title=\"Log out\"\n >\n <ed-icon class=\"shrink-0\" name=\"logout\" [size]=\"24\" color=\"currentColor\" />\n <span class=\"min-w-0 flex-1\">Log out</span>\n </button>\n </div>\n }\n</nav>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n\n<eds-agent-settings-drawer />\n", styles: [":host .ai-receptionist-nav-item .menu-item-icon{background-color:#ad46ff1a;border-radius:6px;transition:background-color .2s ease}:host .ai-receptionist-nav-item:hover .menu-item-icon,:host .ai-receptionist-nav-item.active .menu-item-icon,:host .ai-receptionist-nav-item.active:hover .menu-item-icon{background-color:#9810fa}:host .ai-receptionist-nav-item .ai-receptionist-live-dot{position:absolute;top:-2px;right:-2px;width:10px;height:10px;border-radius:999px;background-color:#089e40;border:.5px solid #ffffff;box-sizing:border-box}:host .ai-receptionist-nav-item .ai-receptionist-live-dot.is-offline{background-color:#dc2626}\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: IconComponent, selector: "ed-icon", inputs: ["name", "size", "color", "cssClass", "decorative", "ariaHidden", "ariaLabel", "title", "desc", "focusable", "tabIndex"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: AgentSettingsDrawerComponent, selector: "eds-agent-settings-drawer" }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
6009
6081
|
}
|
|
6010
6082
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: EdsModuleNavComponent, decorators: [{
|
|
6011
6083
|
type: Component,
|
|
@@ -6016,7 +6088,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
6016
6088
|
ModalComponent,
|
|
6017
6089
|
AgentSettingsDrawerComponent,
|
|
6018
6090
|
EdButtonComponent,
|
|
6019
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'block shrink-0' }, template: "<nav\n class=\"sticky top-0 flex h-screen min-h-screen w-52 flex-col overflow-y-auto border-r border-base bg-white \"\n aria-label=\"AI Receptionist\"\n>\n <div class=\"flex min-h-0 flex-1 flex-col \">\n <div\n class=\"relative shrink-0 p-sm\"\n data-header-menu\n (document:click)=\"onHeaderMenuDocClick($event)\"\n >\n <button\n type=\"button\"\n class=\"flex w-full cursor-pointer items-center justify-between gap-xs rounded-md border-0 bg-transparent p-2xs text-left
|
|
6091
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'block shrink-0' }, template: "<nav\n class=\"sticky top-0 flex h-screen min-h-screen w-52 flex-col overflow-y-auto border-r border-base bg-white \"\n aria-label=\"AI Receptionist\"\n>\n <div class=\"flex min-h-0 flex-1 flex-col \">\n <div\n class=\"relative shrink-0 p-sm\"\n data-header-menu\n (document:click)=\"onHeaderMenuDocClick($event)\"\n >\n <button\n type=\"button\"\n class=\"ai-receptionist-nav-item flex w-full cursor-pointer items-center justify-between gap-xs rounded-md border-0 bg-transparent p-2xs text-left\"\n [class.active]=\"brandIconActive()\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-expanded]=\"headerMenuOpen()\"\n aria-haspopup=\"menu\"\n (mouseenter)=\"brandIconHovered.set(true)\"\n (mouseleave)=\"brandIconHovered.set(false)\"\n (click)=\"$event.stopPropagation(); toggleHeaderMenu()\"\n >\n <div class=\"flex min-w-0 flex-1 items-center gap-xs\">\n <span\n class=\"menu-item-icon relative flex h-9 w-9 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n <ed-icon\n name=\"ai_headset\"\n [size]=\"20\"\n [color]=\"brandIconColor()\"\n />\n @if (online() === true) {\n <span class=\"ai-receptionist-live-dot\"></span>\n } @else if (online() === false) {\n <span class=\"ai-receptionist-live-dot is-offline\"></span>\n }\n </span>\n <span class=\"min-w-0 truncate fs-base font-semibold text-dark\">AI Receptionist</span>\n </div>\n <ed-icon\n name=\"caret_down\"\n [size]=\"16\"\n color=\"currentColor\"\n class=\"shrink-0 text-light\"\n aria-hidden=\"true\"\n />\n </button>\n @if (headerMenuOpen()) {\n <div\n class=\"fade-in absolute right-0 top-full z-20 mt-2xs flex w-44 origin-top flex-col gap-2xs rounded-md border border-light bg-white p-2xs shadow-md\"\n role=\"menu\"\n aria-label=\"AI Receptionist menu\"\n >\n <a\n class=\"flex w-full items-center rounded-sm px-sm py-xs fs-sm text-dark no-underline transition-colors hover:bg-lighter\"\n routerLink=\"audit\"\n routerLinkActive=\"bg-lighter\"\n role=\"menuitem\"\n (click)=\"closeHeaderMenu()\"\n >\n Audit Logs\n </a>\n <button\n type=\"button\"\n class=\"flex w-full cursor-pointer items-center rounded-sm border-0 bg-transparent px-sm py-xs text-left fs-sm text-dark transition-colors hover:bg-lighter\"\n role=\"menuitem\"\n (click)=\"openSettingsDrawer()\"\n >\n Settings\n </button>\n <div class=\"w-full border-b border-light\" role=\"presentation\"></div>\n <div class=\"flex w-full items-center gap-2xs px-sm py-xs\" role=\"menuitem\">\n <span class=\"min-w-0 flex-1 fs-sm text-dark\">\n @if (online() === true) {\n Agent is Live\n } @else if (online() === false) {\n Agent is Offline\n } @else {\n Agent status\n }\n </span>\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent p-2xs transition-colors disabled:cursor-not-allowed disabled:opacity-55\"\n [class.bg-primary]=\"online() === true\"\n [class.justify-end]=\"online() === true\"\n [class.bg-lighter]=\"online() !== true\"\n [class.justify-start]=\"online() !== true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n [attr.aria-label]=\"online() === true ? 'Turn off AI receptionist' : 'Turn on AI receptionist'\"\n (click)=\"$event.stopPropagation(); onAgentToggleClick()\"\n >\n <span class=\"block h-3 w-3 rounded-full bg-white\"></span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"w-full border-b border-light\" role=\"presentation\"></div>\n\n <ul role=\"menu\" class=\"m-0 list-none p-0\">\n @for (item of receptionistNav; track item.id) {\n <li\n tabindex=\"0\"\n role=\"menuitem\"\n class=\"cursor-pointer px-base py-xs text-left fs-base leading-[1.8] hover:bg-lighter active:bg-base\"\n [attr.data-value]=\"item.label\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"bg-light\"\n [routerLinkActiveOptions]=\"{ exact: item.route === 'dashboard' }\"\n (keydown.enter)=\"$any($event.target)?.click()\"\n >{{ item.label }}</li>\n }\n </ul>\n\n @if (includePracticeNav) {\n <p class=\"mb-0 mt-0 fs-sm font-semibold text-light\">Practice</p>\n <ul role=\"menu\" class=\"m-0 list-none p-0\">\n @for (item of practiceNav; track item.id) {\n @if (showNavItem(item)) {\n <li\n tabindex=\"0\"\n role=\"menuitem\"\n class=\"cursor-pointer px-base py-xs text-left fs-base leading-[1.8] hover:bg-lighter active:bg-base\"\n [attr.data-value]=\"item.label\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"bg-light\"\n (keydown.enter)=\"$any($event.target)?.click()\"\n >{{ item.label }}</li>\n }\n }\n </ul>\n }\n </div>\n\n @if (showLogout()) {\n <div class=\"mt-auto border-t border-light pt-sm\">\n @if (userEmail(); as email) {\n <p class=\"mb-xs truncate px-xs fs-xs text-light\" [title]=\"email\">{{ email }}</p>\n }\n <button\n type=\"button\"\n class=\"group/link relative flex h-10 w-full cursor-pointer items-center gap-xs rounded-md border-0 bg-transparent px-xs text-left fs-base font-medium text-light transition-colors hover:bg-lighter hover:text-dark\"\n (click)=\"logout()\"\n aria-label=\"Log out\"\n title=\"Log out\"\n >\n <ed-icon class=\"shrink-0\" name=\"logout\" [size]=\"24\" color=\"currentColor\" />\n <span class=\"min-w-0 flex-1\">Log out</span>\n </button>\n </div>\n }\n</nav>\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 the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n\n<eds-agent-settings-drawer />\n", styles: [":host .ai-receptionist-nav-item .menu-item-icon{background-color:#ad46ff1a;border-radius:6px;transition:background-color .2s ease}:host .ai-receptionist-nav-item:hover .menu-item-icon,:host .ai-receptionist-nav-item.active .menu-item-icon,:host .ai-receptionist-nav-item.active:hover .menu-item-icon{background-color:#9810fa}:host .ai-receptionist-nav-item .ai-receptionist-live-dot{position:absolute;top:-2px;right:-2px;width:10px;height:10px;border-radius:999px;background-color:#089e40;border:.5px solid #ffffff;box-sizing:border-box}:host .ai-receptionist-nav-item .ai-receptionist-live-dot.is-offline{background-color:#dc2626}\n"] }]
|
|
6020
6092
|
}], ctorParameters: () => [] });
|
|
6021
6093
|
|
|
6022
6094
|
/**
|
|
@@ -6500,7 +6572,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6500
6572
|
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
6501
6573
|
{
|
|
6502
6574
|
path: 'dashboard',
|
|
6503
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-
|
|
6575
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs').then((m) => m.DashboardPageComponent),
|
|
6504
6576
|
},
|
|
6505
6577
|
{
|
|
6506
6578
|
path: 'live',
|
|
@@ -6509,37 +6581,37 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6509
6581
|
{
|
|
6510
6582
|
path: '',
|
|
6511
6583
|
pathMatch: 'full',
|
|
6512
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-
|
|
6584
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-BLJTOY-p.mjs').then((m) => m.LiveCallsPageComponent),
|
|
6513
6585
|
},
|
|
6514
6586
|
{
|
|
6515
6587
|
path: ':id',
|
|
6516
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-
|
|
6588
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-MS9bpOBv.mjs').then((m) => m.LiveMonitorPageComponent),
|
|
6517
6589
|
},
|
|
6518
6590
|
],
|
|
6519
6591
|
},
|
|
6520
6592
|
{
|
|
6521
6593
|
path: 'history',
|
|
6522
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-
|
|
6594
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs').then((m) => m.CallHistoryPageComponent),
|
|
6523
6595
|
},
|
|
6524
6596
|
{
|
|
6525
6597
|
path: 'history/:id',
|
|
6526
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-
|
|
6598
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-pRSrt5C2.mjs').then((m) => m.CallHistoryDetailPageComponent),
|
|
6527
6599
|
},
|
|
6528
6600
|
{
|
|
6529
6601
|
path: 'activities',
|
|
6530
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-
|
|
6602
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs').then((m) => m.ActivitiesPageComponent),
|
|
6531
6603
|
},
|
|
6532
6604
|
{
|
|
6533
6605
|
path: 'tasks',
|
|
6534
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-
|
|
6606
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs').then((m) => m.TasksPageComponent),
|
|
6535
6607
|
},
|
|
6536
6608
|
{
|
|
6537
6609
|
path: 'analytics',
|
|
6538
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-
|
|
6610
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs').then((m) => m.AnalyticsPageComponent),
|
|
6539
6611
|
},
|
|
6540
6612
|
{
|
|
6541
6613
|
path: 'audit',
|
|
6542
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-
|
|
6614
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs').then((m) => m.AuditPageComponent),
|
|
6543
6615
|
},
|
|
6544
6616
|
{
|
|
6545
6617
|
path: 'settings',
|
|
@@ -6547,7 +6619,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6547
6619
|
},
|
|
6548
6620
|
{
|
|
6549
6621
|
path: 'settings/outbound-purposes',
|
|
6550
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-
|
|
6622
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs').then((m) => m.OutboundPurposesPageComponent),
|
|
6551
6623
|
},
|
|
6552
6624
|
{
|
|
6553
6625
|
path: 'knowledge',
|
|
@@ -6556,7 +6628,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6556
6628
|
},
|
|
6557
6629
|
{
|
|
6558
6630
|
path: 'wizard',
|
|
6559
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-
|
|
6631
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs').then((m) => m.WizardPageComponent),
|
|
6560
6632
|
},
|
|
6561
6633
|
];
|
|
6562
6634
|
/**
|
|
@@ -6618,4 +6690,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
6618
6690
|
*/
|
|
6619
6691
|
|
|
6620
6692
|
export { AiReceptionistApiService as A, ToastContainerComponent as B, VOICE_RECEPTIONIST_FEATURE_ROUTES as C, DashboardSocketService as D, EdAvatarComponent as E, VOICE_RECEPTIONIST_ROUTES as F, VOICE_RECEPTIONIST_SESSION as G, provideVoiceReceptionist as H, IconComponent as I, LiveCallsListService as L, ModalComponent as M, ToastService as T, VOICE_RECEPTIONIST_CONFIG as V, AgentOnlineService as a, ageFromDob as b, callerMetaLine as c, historyResultChips as d, historyResolutionLabel as e, EdButtonComponent as f, EdSkeletonComponent as g, historyResolution as h, isUnknownCaller as i, toE164Phone as j, resolveToolKey as k, resolveToolDisplayName as l, toolEventTrackKey as m, DEFAULT_PAGE_SIZE as n, totalPages as o, EdIconButtonComponent as p, activityTypeLabel as q, resultChipIcon as r, ViewportPortalDirective as s, triggerBlobDownload as t, initialsFromName as u, DEFAULT_PLACEHOLDERS as v, EdsAppShellComponent as w, EdsBadgeComponent as x, EdsButtonComponent as y, EdsRouteProgressComponent as z };
|
|
6621
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-
|
|
6693
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs.map
|