@edvak99/voice-receptionist-widget 1.1.26 → 1.1.27
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-CU_0syoU.mjs +537 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CU_0syoU.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C_YenFDP.mjs +435 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C_YenFDP.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-BdsYgTWW.mjs +115 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-BdsYgTWW.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-pRSrt5C2.mjs → edvak99-voice-receptionist-widget-call-history-detail-page.component-CBxMXdsL.mjs} +4 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-pRSrt5C2.mjs.map → edvak99-voice-receptionist-widget-call-history-detail-page.component-CBxMXdsL.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B79RtjPX.mjs +266 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B79RtjPX.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-D9hxkypQ.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-BCdmBPbs.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-D9hxkypQ.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-BCdmBPbs.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs → edvak99-voice-receptionist-widget-call-review.service-CB5SQ3Md.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CB5SQ3Md.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs → edvak99-voice-receptionist-widget-dashboard-page.component-o7i5Hd2a.mjs} +4 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs.map → edvak99-voice-receptionist-widget-dashboard-page.component-o7i5Hd2a.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-eds-search-field.component-BDGCmkoP.mjs → edvak99-voice-receptionist-widget-eds-search-field.component-CjF433P4.mjs} +8 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-CjF433P4.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs → edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BxX138_W.mjs} +29 -239
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BxX138_W.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-page.component-BLJTOY-p.mjs → edvak99-voice-receptionist-widget-live-calls-page.component--IvghBHo.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component--IvghBHo.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-MS9bpOBv.mjs → edvak99-voice-receptionist-widget-live-monitor-page.component-BGkh-tr2.mjs} +3 -3
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-MS9bpOBv.mjs.map → edvak99-voice-receptionist-widget-live-monitor-page.component-BGkh-tr2.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-DVgtSIve.mjs} +4 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs.map → edvak99-voice-receptionist-widget-outbound-purposes-page.component-DVgtSIve.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-CAsLgIJB.mjs → edvak99-voice-receptionist-widget-task-detail-drawer.component-XwT0UJ4s.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-CAsLgIJB.mjs.map → edvak99-voice-receptionist-widget-task-detail-drawer.component-XwT0UJ4s.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs → edvak99-voice-receptionist-widget-tasks-page.component-DNyrZJkS.mjs} +3 -3
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs.map → edvak99-voice-receptionist-widget-tasks-page.component-DNyrZJkS.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs → edvak99-voice-receptionist-widget-wizard-page.component-CR7DkO2R.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-CR7DkO2R.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/models/ai-receptionist.types.d.ts +0 -11
- package/lib/models/api-dto.types.d.ts +0 -10
- package/lib/pages/activities-page/activities-page.component.d.ts +1 -0
- package/lib/pages/audit-page/audit-page.component.d.ts +5 -2
- package/lib/pages/call-history-page/call-history-page.component.d.ts +5 -2
- package/lib/pages/settings-page/settings-page.component.d.ts +1 -12
- package/lib/services/ai-receptionist-api.mappers.d.ts +1 -2
- package/lib/services/ai-receptionist-api.mock.d.ts +1 -8
- package/lib/services/ai-receptionist-api.service.d.ts +1 -12
- package/lib/ui/eds-search-field.component.d.ts +1 -0
- package/package.json +1 -1
- package/styles/embed.scss +6 -8
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs +0 -533
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs +0 -435
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs +0 -96
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs +0 -254
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-BDGCmkoP.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-BLJTOY-p.mjs.map +0 -1
|
@@ -1221,57 +1221,6 @@ function humanizeSettingsKey(key) {
|
|
|
1221
1221
|
.trim()
|
|
1222
1222
|
.replace(/^\w/, (c) => c.toUpperCase());
|
|
1223
1223
|
}
|
|
1224
|
-
const DEFAULT_BOOKING_MODE_OPTIONS = [
|
|
1225
|
-
{
|
|
1226
|
-
value: 'confirmed',
|
|
1227
|
-
label: 'Confirm appointment',
|
|
1228
|
-
description: 'Creates a live appointment in the EHR (create-new-appointment). The caller hears that they are booked.',
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
value: 'pending',
|
|
1232
|
-
label: 'Pending appointment',
|
|
1233
|
-
description: 'Submits an Online Request for staff review (req-auto-new-appointment). Do not tell the caller the visit is confirmed.',
|
|
1234
|
-
},
|
|
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
|
-
}
|
|
1251
|
-
function mapBookingModeSettings(body) {
|
|
1252
|
-
const raw = unwrapBookingModePayload(body);
|
|
1253
|
-
const mode = String(raw.bookingMode ?? '').trim().toLowerCase();
|
|
1254
|
-
const bookingMode = mode === 'pending' ? 'pending' : 'confirmed';
|
|
1255
|
-
const options = Array.isArray(raw.options) && raw.options.length
|
|
1256
|
-
? raw.options
|
|
1257
|
-
.map((o) => {
|
|
1258
|
-
const value = String(o?.value ?? '').trim().toLowerCase();
|
|
1259
|
-
if (value !== 'confirmed' && value !== 'pending')
|
|
1260
|
-
return null;
|
|
1261
|
-
return {
|
|
1262
|
-
value: value,
|
|
1263
|
-
label: String(o?.label ?? '').trim() ||
|
|
1264
|
-
(value === 'pending' ? 'Pending appointment' : 'Confirm appointment'),
|
|
1265
|
-
description: String(o?.description ?? '').trim(),
|
|
1266
|
-
};
|
|
1267
|
-
})
|
|
1268
|
-
.filter((o) => !!o)
|
|
1269
|
-
: DEFAULT_BOOKING_MODE_OPTIONS;
|
|
1270
|
-
return {
|
|
1271
|
-
bookingMode,
|
|
1272
|
-
options: options.length ? options : DEFAULT_BOOKING_MODE_OPTIONS,
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
1224
|
/** Map frontend AgentSettings → backend PUT body section keys. */
|
|
1276
1225
|
function mapSettingsToRequest(settings) {
|
|
1277
1226
|
return {
|
|
@@ -2416,7 +2365,6 @@ class AiReceptionistApiMockService {
|
|
|
2416
2365
|
this.tasks = signal(cloneTasks());
|
|
2417
2366
|
this.history = signal(cloneHistory());
|
|
2418
2367
|
this.settings = signal(structuredClone(SEED_SETTINGS));
|
|
2419
|
-
this.bookingMode = 'confirmed';
|
|
2420
2368
|
this.status = signal({
|
|
2421
2369
|
agentOn: true,
|
|
2422
2370
|
agentOnExplicit: true,
|
|
@@ -2737,27 +2685,6 @@ class AiReceptionistApiMockService {
|
|
|
2737
2685
|
this.settings.set(saved);
|
|
2738
2686
|
return this.mock(saved);
|
|
2739
2687
|
}
|
|
2740
|
-
getBookingMode() {
|
|
2741
|
-
return this.mock({
|
|
2742
|
-
bookingMode: this.bookingMode,
|
|
2743
|
-
options: [
|
|
2744
|
-
{
|
|
2745
|
-
value: 'confirmed',
|
|
2746
|
-
label: 'Confirm appointment',
|
|
2747
|
-
description: 'Creates a live appointment in the EHR (create-new-appointment). The caller hears that they are booked.',
|
|
2748
|
-
},
|
|
2749
|
-
{
|
|
2750
|
-
value: 'pending',
|
|
2751
|
-
label: 'Pending appointment',
|
|
2752
|
-
description: 'Submits an Online Request for staff review (req-auto-new-appointment). Do not tell the caller the visit is confirmed.',
|
|
2753
|
-
},
|
|
2754
|
-
],
|
|
2755
|
-
});
|
|
2756
|
-
}
|
|
2757
|
-
putBookingMode(bookingMode) {
|
|
2758
|
-
this.bookingMode = bookingMode === 'pending' ? 'pending' : 'confirmed';
|
|
2759
|
-
return this.getBookingMode();
|
|
2760
|
-
}
|
|
2761
2688
|
getVoicePromptDefault() {
|
|
2762
2689
|
return of({
|
|
2763
2690
|
defaultVoiceSystemPrompt: 'You are an AI receptionist for a medical practice. Be warm, concise, and HIPAA-aware.',
|
|
@@ -3421,22 +3348,6 @@ class AiReceptionistApiService {
|
|
|
3421
3348
|
.put(`${this.arBase}/settings`, { settings: mapSettingsToRequest(next) })
|
|
3422
3349
|
.pipe(switchMap(() => this.getSettings()), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to save agent settings')))));
|
|
3423
3350
|
}
|
|
3424
|
-
/**
|
|
3425
|
-
* Practice-level Confirm vs Pending booking mode.
|
|
3426
|
-
* Dedicated endpoint — JWT supplies the practice; do not send practiceId.
|
|
3427
|
-
*/
|
|
3428
|
-
getBookingMode() {
|
|
3429
|
-
if (this.useMock)
|
|
3430
|
-
return this.mock.getBookingMode();
|
|
3431
|
-
return this.http.get(`${this.arBase}/settings/booking-mode`).pipe(map((body) => ({ data: mapBookingModeSettings(body) })), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to load booking mode')))));
|
|
3432
|
-
}
|
|
3433
|
-
putBookingMode(bookingMode) {
|
|
3434
|
-
if (this.useMock)
|
|
3435
|
-
return this.mock.putBookingMode(bookingMode);
|
|
3436
|
-
return this.http
|
|
3437
|
-
.put(`${this.arBase}/settings/booking-mode`, { bookingMode })
|
|
3438
|
-
.pipe(map((body) => ({ data: mapBookingModeSettings(body) })), catchError((err) => throwError(() => new Error(this.apiErrorMessage(err, 'Failed to save booking mode')))));
|
|
3439
|
-
}
|
|
3440
3351
|
/** Shipped default voice system prompt + max lengths. */
|
|
3441
3352
|
getVoicePromptDefault() {
|
|
3442
3353
|
if (this.useMock)
|
|
@@ -3757,30 +3668,12 @@ class AiReceptionistApiService {
|
|
|
3757
3668
|
apiErrorMessage(err, fallback) {
|
|
3758
3669
|
const body = err.error;
|
|
3759
3670
|
if (typeof body === 'object' && body && 'message' in body) {
|
|
3760
|
-
|
|
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();
|
|
3671
|
+
return String(body.message);
|
|
3768
3672
|
}
|
|
3673
|
+
if (typeof body === 'string' && body.trim())
|
|
3674
|
+
return body;
|
|
3769
3675
|
return fallback;
|
|
3770
3676
|
}
|
|
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
|
-
}
|
|
3784
3677
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3785
3678
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AiReceptionistApiService, providedIn: 'root' }); }
|
|
3786
3679
|
}
|
|
@@ -4681,11 +4574,11 @@ class KnowledgeTestModalComponent {
|
|
|
4681
4574
|
this.playing.set(false);
|
|
4682
4575
|
}
|
|
4683
4576
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeTestModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4684
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: KnowledgeTestModalComponent, isStandalone: true, selector: "eds-knowledge-test-modal", inputs: { open: "open", whatToSay: "whatToSay", whatNotToSay: "whatNotToSay", voiceId: "voiceId" }, outputs: { closed: "closed" }, usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form id=\"knowledge-test-form\" class=\"flex flex-col gap-4\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <div class=\"mb-sm\">\n <label>What might a caller ask
|
|
4577
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: KnowledgeTestModalComponent, isStandalone: true, selector: "eds-knowledge-test-modal", inputs: { open: "open", whatToSay: "whatToSay", whatNotToSay: "whatNotToSay", voiceId: "voiceId" }, outputs: { closed: "closed" }, usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form id=\"knowledge-test-form\" class=\"flex flex-col gap-4\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <div class=\"mb-sm\">\n <label><span required>What might a caller ask?</span></label>\n <textarea\n class=\"form-textarea min-h-[84px]\"\n rows=\"3\"\n [maxlength]=\"questionMax\"\n [ngModel]=\"question()\"\n (ngModelChange)=\"onQuestionChange($event)\"\n name=\"question\"\n placeholder=\"e.g. What are your Saturday hours? Is parking free?\"\n [disabled]=\"testing()\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"kb-test__hint self-end text-xs\">{{ question().length }}/{{ questionMax }}</span>\n </div>\n\n @if (answer(); as spoken) {\n <div class=\"kb-test__answer rounded-lg border px-4 py-3\" role=\"status\">\n <div class=\"mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide\">\n <ed-icon name=\"graph_new\" [size]=\"18\" />\n <span>Agent reply</span>\n @if (playing()) {\n <span class=\"kb-test__playing ml-auto font-medium normal-case tracking-normal\">Playing\u2026</span>\n } @else {\n <button type=\"button\" class=\"kb-test__replay ml-auto cursor-pointer border-none bg-transparent px-1 py-1 text-[13px] font-semibold normal-case tracking-normal\" (click)=\"replay()\">\n Replay\n </button>\n }\n </div>\n <p class=\"m-0 text-sm leading-[1.55]\">{{ spoken }}</p>\n </div>\n }\n </form>\n\n <div modalFooter class=\"border-t border-base bg-white px-6 py-4\">\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"close()\" [disabled]=\"testing()\">\n Close\n </ed-button>\n <ed-button\n type=\"button\"\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"testing() || !question().trim()\"\n (click$)=\"runTest()\"\n >\n @if (testing()) {\n Generating\u2026\n } @else {\n <ed-icon slot=\"prefix\" name=\"play_arrow\" [size]=\"16\" />\n Test\n }\n </ed-button>\n </div>\n </div>\n </app-modal>\n}\n", styles: [".kb-test__hint{color:var(--eds-color-text-muted, #64748b)}.kb-test__answer{border-color:var(--eds-color-border, #e2e8f0);background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__answer-head{color:var(--eds-color-text-muted, #64748b)}.kb-test__playing,.kb-test__replay{color:var(--eds-primary, #2563eb)}.kb-test__answer-text{color:var(--eds-color-text, #0f172a)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { 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$"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
4685
4578
|
}
|
|
4686
4579
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeTestModalComponent, decorators: [{
|
|
4687
4580
|
type: Component,
|
|
4688
|
-
args: [{ selector: 'eds-knowledge-test-modal', imports: [FormsModule, ModalComponent, IconComponent, EdButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form id=\"knowledge-test-form\" class=\"flex flex-col gap-4\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <div class=\"mb-sm\">\n <label>What might a caller ask
|
|
4581
|
+
args: [{ selector: 'eds-knowledge-test-modal', imports: [FormsModule, ModalComponent, IconComponent, EdButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form id=\"knowledge-test-form\" class=\"flex flex-col gap-4\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <div class=\"mb-sm\">\n <label><span required>What might a caller ask?</span></label>\n <textarea\n class=\"form-textarea min-h-[84px]\"\n rows=\"3\"\n [maxlength]=\"questionMax\"\n [ngModel]=\"question()\"\n (ngModelChange)=\"onQuestionChange($event)\"\n name=\"question\"\n placeholder=\"e.g. What are your Saturday hours? Is parking free?\"\n [disabled]=\"testing()\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"kb-test__hint self-end text-xs\">{{ question().length }}/{{ questionMax }}</span>\n </div>\n\n @if (answer(); as spoken) {\n <div class=\"kb-test__answer rounded-lg border px-4 py-3\" role=\"status\">\n <div class=\"mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide\">\n <ed-icon name=\"graph_new\" [size]=\"18\" />\n <span>Agent reply</span>\n @if (playing()) {\n <span class=\"kb-test__playing ml-auto font-medium normal-case tracking-normal\">Playing\u2026</span>\n } @else {\n <button type=\"button\" class=\"kb-test__replay ml-auto cursor-pointer border-none bg-transparent px-1 py-1 text-[13px] font-semibold normal-case tracking-normal\" (click)=\"replay()\">\n Replay\n </button>\n }\n </div>\n <p class=\"m-0 text-sm leading-[1.55]\">{{ spoken }}</p>\n </div>\n }\n </form>\n\n <div modalFooter class=\"border-t border-base bg-white px-6 py-4\">\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"close()\" [disabled]=\"testing()\">\n Close\n </ed-button>\n <ed-button\n type=\"button\"\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"testing() || !question().trim()\"\n (click$)=\"runTest()\"\n >\n @if (testing()) {\n Generating\u2026\n } @else {\n <ed-icon slot=\"prefix\" name=\"play_arrow\" [size]=\"16\" />\n Test\n }\n </ed-button>\n </div>\n </div>\n </app-modal>\n}\n", styles: [".kb-test__hint{color:var(--eds-color-text-muted, #64748b)}.kb-test__answer{border-color:var(--eds-color-border, #e2e8f0);background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__answer-head{color:var(--eds-color-text-muted, #64748b)}.kb-test__playing,.kb-test__replay{color:var(--eds-primary, #2563eb)}.kb-test__answer-text{color:var(--eds-color-text, #0f172a)}\n"] }]
|
|
4689
4582
|
}], propDecorators: { open: [{
|
|
4690
4583
|
type: Input,
|
|
4691
4584
|
args: [{ required: true }]
|
|
@@ -5183,12 +5076,6 @@ class SettingsPageComponent {
|
|
|
5183
5076
|
this.confirmOffOpen = signal(false);
|
|
5184
5077
|
this.voicePreviewBusy = signal(false);
|
|
5185
5078
|
this.voicePreviewPlaying = signal(false);
|
|
5186
|
-
/** Isolated from main settings body — practice-level EHR booking write path. */
|
|
5187
|
-
this.bookingMode = signal('confirmed');
|
|
5188
|
-
this.bookingModeOptions = signal([]);
|
|
5189
|
-
this.bookingModeLoading = signal(true);
|
|
5190
|
-
this.bookingModeError = signal(null);
|
|
5191
|
-
this.originalBookingMode = null;
|
|
5192
5079
|
this.online = this.agentOnline.online;
|
|
5193
5080
|
this.agentLoading = this.agentOnline.loading;
|
|
5194
5081
|
this.toggling = this.agentOnline.toggling;
|
|
@@ -5285,7 +5172,6 @@ class SettingsPageComponent {
|
|
|
5285
5172
|
next: ({ data }) => this.locations.set(data ?? []),
|
|
5286
5173
|
error: () => this.locations.set([]),
|
|
5287
5174
|
});
|
|
5288
|
-
this.loadBookingMode();
|
|
5289
5175
|
}
|
|
5290
5176
|
ngOnDestroy() {
|
|
5291
5177
|
this.stopVoicePreview();
|
|
@@ -5310,80 +5196,9 @@ class SettingsPageComponent {
|
|
|
5310
5196
|
if (!this.originalSettings)
|
|
5311
5197
|
return;
|
|
5312
5198
|
this.settings.set(structuredClone(this.originalSettings));
|
|
5313
|
-
if (this.originalBookingMode)
|
|
5314
|
-
this.bookingMode.set(this.originalBookingMode);
|
|
5315
|
-
this.bookingModeError.set(null);
|
|
5316
5199
|
this.locationsMenuOpen.set(false);
|
|
5317
5200
|
this.stopVoicePreview();
|
|
5318
5201
|
}
|
|
5319
|
-
loadBookingMode() {
|
|
5320
|
-
this.bookingModeLoading.set(true);
|
|
5321
|
-
this.bookingModeError.set(null);
|
|
5322
|
-
this.api.getBookingMode().subscribe({
|
|
5323
|
-
next: ({ data }) => {
|
|
5324
|
-
this.bookingMode.set(data.bookingMode);
|
|
5325
|
-
this.originalBookingMode = data.bookingMode;
|
|
5326
|
-
this.bookingModeOptions.set(data.options);
|
|
5327
|
-
this.bookingModeLoading.set(false);
|
|
5328
|
-
},
|
|
5329
|
-
error: (err) => {
|
|
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
|
-
}
|
|
5345
|
-
this.bookingModeError.set(this.asErrorText(err, 'Could not load booking mode for this practice.'));
|
|
5346
|
-
},
|
|
5347
|
-
});
|
|
5348
|
-
}
|
|
5349
|
-
selectBookingMode(mode) {
|
|
5350
|
-
if (this.saving() || this.bookingMode() === mode)
|
|
5351
|
-
return;
|
|
5352
|
-
this.bookingMode.set(mode);
|
|
5353
|
-
this.bookingModeError.set(null);
|
|
5354
|
-
}
|
|
5355
|
-
asErrorText(err, fallback) {
|
|
5356
|
-
if (err instanceof Error && err.message && err.message !== '[object Object]') {
|
|
5357
|
-
return err.message;
|
|
5358
|
-
}
|
|
5359
|
-
if (typeof err === 'string' && err.trim())
|
|
5360
|
-
return err.trim();
|
|
5361
|
-
if (err && typeof err === 'object') {
|
|
5362
|
-
const o = err;
|
|
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;
|
|
5369
|
-
}
|
|
5370
|
-
return fallback;
|
|
5371
|
-
}
|
|
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;
|
|
5386
|
-
}
|
|
5387
5202
|
updateField(section, key, value) {
|
|
5388
5203
|
const current = this.settings();
|
|
5389
5204
|
if (!current || isFieldReadOnly(section, key))
|
|
@@ -5602,48 +5417,25 @@ class SettingsPageComponent {
|
|
|
5602
5417
|
payload.transcripts['retainDays'] = Math.max(recDays, txDays);
|
|
5603
5418
|
this.restoreReadOnlyFields(payload, orig);
|
|
5604
5419
|
this.saving.set(true);
|
|
5605
|
-
this.bookingModeError.set(null);
|
|
5606
|
-
const nextBookingMode = this.bookingMode();
|
|
5607
|
-
const bookingModeDirty = this.originalBookingMode != null && nextBookingMode !== this.originalBookingMode;
|
|
5608
5420
|
this.api.putSettings(payload).subscribe({
|
|
5609
5421
|
next: ({ data }) => {
|
|
5610
5422
|
this.originalSettings = structuredClone(data);
|
|
5611
5423
|
this.settings.set(structuredClone(data));
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
this.
|
|
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
|
-
});
|
|
5424
|
+
this.saving.set(false);
|
|
5425
|
+
this.saved.set(true);
|
|
5426
|
+
this.toast.show('Agent settings saved', 'success');
|
|
5427
|
+
setTimeout(() => this.saved.set(false), 2000);
|
|
5428
|
+
this.savedSuccessfully.emit();
|
|
5632
5429
|
},
|
|
5633
5430
|
error: (err) => {
|
|
5634
5431
|
this.saving.set(false);
|
|
5635
|
-
const message =
|
|
5432
|
+
const message = err instanceof Error && err.message.trim()
|
|
5433
|
+
? err.message
|
|
5434
|
+
: 'Failed to save agent settings';
|
|
5636
5435
|
this.toast.show(message, 'error');
|
|
5637
5436
|
},
|
|
5638
5437
|
});
|
|
5639
5438
|
}
|
|
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
|
-
}
|
|
5647
5439
|
restoreReadOnlyFields(payload, orig) {
|
|
5648
5440
|
const editableSections = [
|
|
5649
5441
|
'general',
|
|
@@ -5673,7 +5465,7 @@ class SettingsPageComponent {
|
|
|
5673
5465
|
}
|
|
5674
5466
|
}
|
|
5675
5467
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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 }); }
|
|
5468
|
+
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" }, 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><span required>What to say</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><span required>What not to say</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 <span required>Name</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><span required>Voice</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><span required>Inbound Greetings</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\"><span required>AI Answer Mode</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><span required>AI Phone Number</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><span required>Transfer Number</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><span required>Ringing Time Wait</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><span required>Location Handled</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><span required>Max AI Call Duration</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><span required>Silence Timeout</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><span required>After-Hours Behaviour</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-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><span required>Keep Call Recordings</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><span required>Keep Transcripts</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}.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)}.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 }); }
|
|
5677
5469
|
}
|
|
5678
5470
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, decorators: [{
|
|
5679
5471
|
type: Component,
|
|
@@ -5685,9 +5477,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
5685
5477
|
KnowledgeTestModalComponent,
|
|
5686
5478
|
ModalComponent,
|
|
5687
5479
|
EdSkeletonComponent,
|
|
5688
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
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"] }]
|
|
5480
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, 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><span required>What to say</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><span required>What not to say</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 <span required>Name</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><span required>Voice</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><span required>Inbound Greetings</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\"><span required>AI Answer Mode</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><span required>AI Phone Number</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><span required>Transfer Number</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><span required>Ringing Time Wait</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><span required>Location Handled</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><span required>Max AI Call Duration</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><span required>Silence Timeout</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><span required>After-Hours Behaviour</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-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><span required>Keep Call Recordings</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><span required>Keep Transcripts</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}.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)}.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"] }]
|
|
5691
5481
|
}] });
|
|
5692
5482
|
|
|
5693
5483
|
var settingsPage_component = /*#__PURE__*/Object.freeze({
|
|
@@ -5964,7 +5754,7 @@ class AgentSettingsDrawerComponent {
|
|
|
5964
5754
|
}
|
|
5965
5755
|
}
|
|
5966
5756
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentSettingsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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-
|
|
5757
|
+
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-visible 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 }); }
|
|
5968
5758
|
}
|
|
5969
5759
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentSettingsDrawerComponent, decorators: [{
|
|
5970
5760
|
type: Component,
|
|
@@ -5973,7 +5763,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
5973
5763
|
EdButtonComponent,
|
|
5974
5764
|
EdIconButtonComponent,
|
|
5975
5765
|
ViewportPortalDirective,
|
|
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-
|
|
5766
|
+
], 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-visible 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"] }]
|
|
5977
5767
|
}] });
|
|
5978
5768
|
|
|
5979
5769
|
class EdsModuleNavComponent {
|
|
@@ -6572,7 +6362,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6572
6362
|
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
6573
6363
|
{
|
|
6574
6364
|
path: 'dashboard',
|
|
6575
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-
|
|
6365
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-o7i5Hd2a.mjs').then((m) => m.DashboardPageComponent),
|
|
6576
6366
|
},
|
|
6577
6367
|
{
|
|
6578
6368
|
path: 'live',
|
|
@@ -6581,37 +6371,37 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6581
6371
|
{
|
|
6582
6372
|
path: '',
|
|
6583
6373
|
pathMatch: 'full',
|
|
6584
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component
|
|
6374
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component--IvghBHo.mjs').then((m) => m.LiveCallsPageComponent),
|
|
6585
6375
|
},
|
|
6586
6376
|
{
|
|
6587
6377
|
path: ':id',
|
|
6588
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-
|
|
6378
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-BGkh-tr2.mjs').then((m) => m.LiveMonitorPageComponent),
|
|
6589
6379
|
},
|
|
6590
6380
|
],
|
|
6591
6381
|
},
|
|
6592
6382
|
{
|
|
6593
6383
|
path: 'history',
|
|
6594
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-
|
|
6384
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-B79RtjPX.mjs').then((m) => m.CallHistoryPageComponent),
|
|
6595
6385
|
},
|
|
6596
6386
|
{
|
|
6597
6387
|
path: 'history/:id',
|
|
6598
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-
|
|
6388
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-CBxMXdsL.mjs').then((m) => m.CallHistoryDetailPageComponent),
|
|
6599
6389
|
},
|
|
6600
6390
|
{
|
|
6601
6391
|
path: 'activities',
|
|
6602
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-
|
|
6392
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-CU_0syoU.mjs').then((m) => m.ActivitiesPageComponent),
|
|
6603
6393
|
},
|
|
6604
6394
|
{
|
|
6605
6395
|
path: 'tasks',
|
|
6606
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-
|
|
6396
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-DNyrZJkS.mjs').then((m) => m.TasksPageComponent),
|
|
6607
6397
|
},
|
|
6608
6398
|
{
|
|
6609
6399
|
path: 'analytics',
|
|
6610
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-
|
|
6400
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-C_YenFDP.mjs').then((m) => m.AnalyticsPageComponent),
|
|
6611
6401
|
},
|
|
6612
6402
|
{
|
|
6613
6403
|
path: 'audit',
|
|
6614
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-
|
|
6404
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-BdsYgTWW.mjs').then((m) => m.AuditPageComponent),
|
|
6615
6405
|
},
|
|
6616
6406
|
{
|
|
6617
6407
|
path: 'settings',
|
|
@@ -6619,7 +6409,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6619
6409
|
},
|
|
6620
6410
|
{
|
|
6621
6411
|
path: 'settings/outbound-purposes',
|
|
6622
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-
|
|
6412
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-DVgtSIve.mjs').then((m) => m.OutboundPurposesPageComponent),
|
|
6623
6413
|
},
|
|
6624
6414
|
{
|
|
6625
6415
|
path: 'knowledge',
|
|
@@ -6628,7 +6418,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6628
6418
|
},
|
|
6629
6419
|
{
|
|
6630
6420
|
path: 'wizard',
|
|
6631
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-
|
|
6421
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-CR7DkO2R.mjs').then((m) => m.WizardPageComponent),
|
|
6632
6422
|
},
|
|
6633
6423
|
];
|
|
6634
6424
|
/**
|
|
@@ -6690,4 +6480,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
6690
6480
|
*/
|
|
6691
6481
|
|
|
6692
6482
|
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 };
|
|
6693
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-
|
|
6483
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BxX138_W.mjs.map
|