@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.
Files changed (56) hide show
  1. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CU_0syoU.mjs +537 -0
  2. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CU_0syoU.mjs.map +1 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C_YenFDP.mjs +435 -0
  4. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C_YenFDP.mjs.map +1 -0
  5. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-BdsYgTWW.mjs +115 -0
  6. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-BdsYgTWW.mjs.map +1 -0
  7. 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
  8. 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
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B79RtjPX.mjs +266 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B79RtjPX.mjs.map +1 -0
  11. package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-D9hxkypQ.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-BCdmBPbs.mjs} +2 -2
  12. 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
  13. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs → edvak99-voice-receptionist-widget-call-review.service-CB5SQ3Md.mjs} +2 -2
  14. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DhLqgadd.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CB5SQ3Md.mjs.map} +1 -1
  15. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs → edvak99-voice-receptionist-widget-dashboard-page.component-o7i5Hd2a.mjs} +4 -4
  16. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-page.component-CoqHpfH4.mjs.map → edvak99-voice-receptionist-widget-dashboard-page.component-o7i5Hd2a.mjs.map} +1 -1
  17. package/fesm2022/{edvak99-voice-receptionist-widget-eds-search-field.component-BDGCmkoP.mjs → edvak99-voice-receptionist-widget-eds-search-field.component-CjF433P4.mjs} +8 -1
  18. package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-CjF433P4.mjs.map +1 -0
  19. 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
  20. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BxX138_W.mjs.map +1 -0
  21. 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
  22. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component--IvghBHo.mjs.map +1 -0
  23. 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
  24. 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
  25. package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Wxq3Aqip.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-DVgtSIve.mjs} +4 -4
  26. 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
  27. package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-CAsLgIJB.mjs → edvak99-voice-receptionist-widget-task-detail-drawer.component-XwT0UJ4s.mjs} +2 -2
  28. 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
  29. package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs → edvak99-voice-receptionist-widget-tasks-page.component-DNyrZJkS.mjs} +3 -3
  30. package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-9X8YjNaO.mjs.map → edvak99-voice-receptionist-widget-tasks-page.component-DNyrZJkS.mjs.map} +1 -1
  31. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs → edvak99-voice-receptionist-widget-wizard-page.component-CR7DkO2R.mjs} +2 -2
  32. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CktmVrGt.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-CR7DkO2R.mjs.map} +1 -1
  33. package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
  34. package/lib/models/ai-receptionist.types.d.ts +0 -11
  35. package/lib/models/api-dto.types.d.ts +0 -10
  36. package/lib/pages/activities-page/activities-page.component.d.ts +1 -0
  37. package/lib/pages/audit-page/audit-page.component.d.ts +5 -2
  38. package/lib/pages/call-history-page/call-history-page.component.d.ts +5 -2
  39. package/lib/pages/settings-page/settings-page.component.d.ts +1 -12
  40. package/lib/services/ai-receptionist-api.mappers.d.ts +1 -2
  41. package/lib/services/ai-receptionist-api.mock.d.ts +1 -8
  42. package/lib/services/ai-receptionist-api.service.d.ts +1 -12
  43. package/lib/ui/eds-search-field.component.d.ts +1 -0
  44. package/package.json +1 -1
  45. package/styles/embed.scss +6 -8
  46. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs +0 -533
  47. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-BlOth9CN.mjs.map +0 -1
  48. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs +0 -435
  49. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Cr02cCqy.mjs.map +0 -1
  50. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs +0 -96
  51. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-IKR5CpVe.mjs.map +0 -1
  52. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs +0 -254
  53. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CtdkYCXC.mjs.map +0 -1
  54. package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-BDGCmkoP.mjs.map +0 -1
  55. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CabSa0_n.mjs.map +0 -1
  56. 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
- const text = this.coerceErrorText(body.message);
3761
- if (text)
3762
- return text;
3763
- }
3764
- if (typeof body === 'string' && body.trim() && body !== '[object Object]')
3765
- return body.trim();
3766
- if (typeof err.message === 'string' && err.message.trim() && err.message !== '[object Object]') {
3767
- return err.message.trim();
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?<span required></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 }); }
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?<span required></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"] }]
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
- if (!bookingModeDirty) {
5613
- this.finishSaveSuccess();
5614
- return;
5615
- }
5616
- this.api.putBookingMode(nextBookingMode).subscribe({
5617
- next: ({ data: booking }) => {
5618
- this.bookingMode.set(booking.bookingMode);
5619
- this.originalBookingMode = booking.bookingMode;
5620
- if (booking.options?.length)
5621
- this.bookingModeOptions.set(booking.options);
5622
- this.finishSaveSuccess();
5623
- },
5624
- error: (err) => {
5625
- this.saving.set(false);
5626
- const text = this.asErrorText(err, 'Failed to save booking mode');
5627
- this.bookingModeError.set(text);
5628
- this.tab.set('phone');
5629
- this.toast.show(`Agent settings saved, but booking mode failed: ${text}`, 'error');
5630
- },
5631
- });
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 = this.asErrorText(err, 'Failed to save agent settings');
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-hidden p-0\">\n <eds-settings-page\n #settingsForm\n presentation=\"drawer\"\n />\n </ed-drawer-body>\n\n <ed-drawer-footer class=\"shrink-0 flex-row-reverse bg-lighter\">\n <ed-button\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"settingsForm.saving()\"\n (click$)=\"settingsForm.save()\"\n >\n {{ settingsForm.saving() ? 'Saving\u2026' : 'Save' }}\n </ed-button>\n <ed-button\n variant=\"default\"\n outline\n size=\"sm\"\n (click$)=\"close()\"\n >\n Cancel\n </ed-button>\n @if (settingsForm.saved()) {\n <span class=\"mr-auto fs-sm font-medium text-green-500\">Saved</span>\n }\n </ed-drawer-footer>\n </ed-drawer>\n </div>\n}\n", styles: ["@charset \"UTF-8\";.eds-drawer-backdrop{background-color:#0f172a66}\n"], dependencies: [{ kind: "component", type: SettingsPageComponent, selector: "eds-settings-page", inputs: ["presentation"], outputs: ["savedSuccessfully"] }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }, { kind: "component", type: EdIconButtonComponent, selector: "ed-icon-button", inputs: ["label", "name", "size", "variant", "allowBubble", "ghost", "outline", "solid", "subtle", "shape", "type", "disabled", "loading", "toggle", "pressed", "id", "title", "nameAttr", "value", "autofocus", "tabIndex"], outputs: ["click", "focus", "blur", "click$", "focus$", "blur$"] }, { kind: "directive", type: ViewportPortalDirective, selector: "[edsViewportPortal]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
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-hidden p-0\">\n <eds-settings-page\n #settingsForm\n presentation=\"drawer\"\n />\n </ed-drawer-body>\n\n <ed-drawer-footer class=\"shrink-0 flex-row-reverse bg-lighter\">\n <ed-button\n variant=\"primary\"\n fill\n size=\"sm\"\n [disabled]=\"settingsForm.saving()\"\n (click$)=\"settingsForm.save()\"\n >\n {{ settingsForm.saving() ? 'Saving\u2026' : 'Save' }}\n </ed-button>\n <ed-button\n variant=\"default\"\n outline\n size=\"sm\"\n (click$)=\"close()\"\n >\n Cancel\n </ed-button>\n @if (settingsForm.saved()) {\n <span class=\"mr-auto fs-sm font-medium text-green-500\">Saved</span>\n }\n </ed-drawer-footer>\n </ed-drawer>\n </div>\n}\n", styles: ["@charset \"UTF-8\";.eds-drawer-backdrop{background-color:#0f172a66}\n"] }]
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-CoqHpfH4.mjs').then((m) => m.DashboardPageComponent),
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-BLJTOY-p.mjs').then((m) => m.LiveCallsPageComponent),
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-MS9bpOBv.mjs').then((m) => m.LiveMonitorPageComponent),
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-CtdkYCXC.mjs').then((m) => m.CallHistoryPageComponent),
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-pRSrt5C2.mjs').then((m) => m.CallHistoryDetailPageComponent),
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-BlOth9CN.mjs').then((m) => m.ActivitiesPageComponent),
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-9X8YjNaO.mjs').then((m) => m.TasksPageComponent),
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-Cr02cCqy.mjs').then((m) => m.AnalyticsPageComponent),
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-IKR5CpVe.mjs').then((m) => m.AuditPageComponent),
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-Wxq3Aqip.mjs').then((m) => m.OutboundPurposesPageComponent),
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-CktmVrGt.mjs').then((m) => m.WizardPageComponent),
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-CabSa0_n.mjs.map
6483
+ //# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BxX138_W.mjs.map