@edvak99/voice-receptionist-widget 1.1.22 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{edvak99-voice-receptionist-widget-activities-page.component-dcDXT2KG.mjs → edvak99-voice-receptionist-widget-activities-page.component-D16UQWsH.mjs} +11 -5
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-D16UQWsH.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-analytics-page.component-B25yZJZz.mjs → edvak99-voice-receptionist-widget-analytics-page.component-DEXdD1mB.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-analytics-page.component-B25yZJZz.mjs.map → edvak99-voice-receptionist-widget-analytics-page.component-DEXdD1mB.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-D-uAGYwd.mjs +89 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-D-uAGYwd.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-BJGzgVxG.mjs → edvak99-voice-receptionist-widget-call-history-detail-page.component-D3GAw7pk.mjs} +30 -11
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-D3GAw7pk.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-page.component-CcwwO5SO.mjs → edvak99-voice-receptionist-widget-call-history-page.component-krL6GnMf.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-krL6GnMf.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-BepxNLNQ.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-C-iLpJhH.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-BepxNLNQ.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-C-iLpJhH.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-nFqc30t3.mjs → edvak99-voice-receptionist-widget-call-review.service-DmvSPr-I.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-nFqc30t3.mjs.map → edvak99-voice-receptionist-widget-call-review.service-DmvSPr-I.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CcphzJ4N.mjs +311 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CcphzJ4N.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CJ1L6QeE.mjs → edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CGbyYMbi.mjs} +14 -14
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CGbyYMbi.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-page.component-MnBMu8iA.mjs → edvak99-voice-receptionist-widget-live-calls-page.component-gTy7IynR.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-page.component-MnBMu8iA.mjs.map → edvak99-voice-receptionist-widget-live-calls-page.component-gTy7IynR.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-BzvMkbC4.mjs → edvak99-voice-receptionist-widget-live-monitor-page.component-C-sDCE3S.mjs} +3 -3
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-BzvMkbC4.mjs.map → edvak99-voice-receptionist-widget-live-monitor-page.component-C-sDCE3S.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Aws6TaaE.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-87FxvbsK.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-Aws6TaaE.mjs.map → edvak99-voice-receptionist-widget-outbound-purposes-page.component-87FxvbsK.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-DVWdArqj.mjs → edvak99-voice-receptionist-widget-task-detail-drawer.component-BNbjcoA2.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-DVWdArqj.mjs.map → edvak99-voice-receptionist-widget-task-detail-drawer.component-BNbjcoA2.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-CBDmGLol.mjs → edvak99-voice-receptionist-widget-tasks-page.component-OP77Shm4.mjs} +3 -3
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-CBDmGLol.mjs.map → edvak99-voice-receptionist-widget-tasks-page.component-OP77Shm4.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C-hdBQIV.mjs → edvak99-voice-receptionist-widget-wizard-page.component-CefGWtVL.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C-hdBQIV.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-CefGWtVL.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +4 -3
- package/lib/pages/activities-page/activities-page.component.d.ts +1 -0
- package/lib/pages/audit-page/audit-page.component.d.ts +0 -2
- package/package.json +1 -1
- package/styles/embed.scss +38 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-dcDXT2KG.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-Df-dtgR3.mjs +0 -96
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-Df-dtgR3.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BJGzgVxG.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CcwwO5SO.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CMgOu0aw.mjs +0 -311
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CMgOu0aw.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CJ1L6QeE.mjs.map +0 -1
|
@@ -5460,7 +5460,7 @@ class SettingsPageComponent {
|
|
|
5460
5460
|
}
|
|
5461
5461
|
}
|
|
5462
5462
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5463
|
-
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>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=\"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}.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 }); }
|
|
5463
|
+
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>What to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span class=\"eds-req\" aria-hidden=\"true\">*</span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\n @if (toggling()) {\n Updating\u2026\n } @else if (online() === true) {\n Agent online\n } @else if (online() === false) {\n Agent offline\n } @else {\n Status unknown\n }\n </span>\n <span class=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\n\n@if (confirmOffOpen()) {\n <app-modal\n title=\"Turn off AI receptionist?\"\n subtitle=\"Inbound callers will be transferred to the front desk instead of the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}.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)}.eds-req{display:inline;margin-left:.2em;color:#e10d0d;font-size:.85em;font-weight:600;line-height:1;vertical-align:text-top;position:static}.eds-req:after{content:none!important}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ed-icon", inputs: ["name", "size", "color", "cssClass", "decorative", "ariaHidden", "ariaLabel", "title", "desc", "focusable", "tabIndex"] }, { kind: "component", type: EdButtonComponent, selector: "ed-button", inputs: ["variant", "size", "disabled", "ghost", "outline", "subtle", "fill", "loading", "block", "elevated", "shape", "type", "href", "routerLink", "rel", "target", "download", "ariaLabel"], outputs: ["click$"] }, { kind: "component", type: KnowledgeTestModalComponent, selector: "eds-knowledge-test-modal", inputs: ["open", "whatToSay", "whatNotToSay", "voiceId"], outputs: ["closed"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: EdSkeletonComponent, selector: "ed-skeleton", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5464
5464
|
}
|
|
5465
5465
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, decorators: [{
|
|
5466
5466
|
type: Component,
|
|
@@ -5472,7 +5472,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
5472
5472
|
KnowledgeTestModalComponent,
|
|
5473
5473
|
ModalComponent,
|
|
5474
5474
|
EdSkeletonComponent,
|
|
5475
|
-
], 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>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=\"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}.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"] }]
|
|
5475
|
+
], 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>What to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatToSay', $event)\"\n placeholder=\"Hours, parking, payment options, arrival instructions, emergencies\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>What not to say<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-56\"\n rows=\"10\"\n [maxlength]=\"knowledgeMax\"\n [ngModel]=\"whatNotToSay()\"\n (ngModelChange)=\"updateKnowledgeField('whatNotToSay', $event)\"\n placeholder=\"Clinical advice, unverified prices, PHI, promises the practice cannot keep\u2026\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </div>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">\n Name<span class=\"eds-req\" aria-hidden=\"true\">*</span>\n </label>\n <ed-icon\n name=\"info\"\n [size]=\"16\"\n class=\"text-light\"\n title=\"How the AI introduces itself on calls\"\n />\n </div>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </div>\n\n <div class=\"flex items-end gap-sm\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-xs\">\n <label>Voice<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm settings-page__select\"\n [ngModel]=\"selectedVoiceId()\"\n (ngModelChange)=\"onVoiceChange($event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.gender }} \u00B7 {{ v.label }}</option>\n }\n </select>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__voice-play inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-base bg-white disabled:cursor-wait disabled:opacity-60\"\n [disabled]=\"!selectedVoiceId() || voicePreviewBusy()\"\n [attr.aria-label]=\"voicePreviewPlaying() ? 'Stop voice preview' : 'Play voice preview'\"\n [title]=\"voicePreviewPlaying() ? 'Stop preview' : 'Play sample'\"\n (click)=\"toggleVoicePreview()\"\n >\n <ed-icon [name]=\"voicePreviewPlaying() ? 'stop' : 'play_arrow'\" [size]=\"20\" />\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Tone</label>\n <div class=\"flex flex-wrap gap-xs\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone cursor-pointer rounded-full border border-transparent bg-lighter px-sm py-2xs fs-xs font-medium text-body\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n [class.font-semibold]=\"selectedTone() === tone.value\"\n [attr.aria-checked]=\"selectedTone() === tone.value\"\n role=\"radio\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Inbound Greetings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n spellcheck=\"false\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"block text-right fs-xs text-light\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"flex flex-wrap items-center gap-xs\">\n <span class=\"fs-xs font-medium text-dark\">Variables:</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip inline-flex h-5 cursor-pointer items-center rounded border border-base bg-white px-2xs fs-xs font-medium text-dark\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </div>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center justify-between gap-xs\">\n <label class=\"m-0\">AI Answer Mode<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <ed-icon name=\"info\" [size]=\"16\" class=\"text-light\" title=\"When the AI picks up inbound calls\" />\n </div>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'answerMode')\"\n (ngModelChange)=\"updateField('phone', 'answerMode', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of answerModeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>AI Phone Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Number patients dial for the AI receptionist. Locked (managed by platform).</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Transfer Number<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Staff / front-desk handoff number. Locked.</span>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Ringing Time Wait<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'ringTimeout')\"\n (ngModelChange)=\"selectNumberField('phone', 'ringTimeout', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of ringTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"event_available\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Create Follow-up Task</span>\n <span class=\"fs-xs text-light\">\n If no one answers in time, the agent auto-creates a high priority task.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n aria-label=\"Create Follow-up Task\"\n (click)=\"updateField('phone', 'createFollowUpTask', !fieldValue('phone', 'createFollowUpTask'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Location Handled<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <div class=\"relative\" data-locations-dd (document:click)=\"onLocationsDocClick($event)\">\n <div\n class=\"form-input flex h-auto w-full cursor-pointer items-center justify-between gap-xs py-2xs text-left\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.opacity-55]=\"locations().length === 0\"\n [attr.aria-expanded]=\"locationsMenuOpen()\"\n [attr.aria-disabled]=\"locations().length === 0\"\n [attr.title]=\"locationSummary()\"\n aria-haspopup=\"listbox\"\n aria-label=\"Locations handled\"\n (click)=\"$event.stopPropagation(); locations().length && toggleLocationsMenu()\"\n (keydown.enter)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n (keydown.space)=\"$event.preventDefault(); locations().length && toggleLocationsMenu()\"\n >\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2xs overflow-hidden\">\n @if (locations().length === 0) {\n <span class=\"fs-sm text-light\">No locations available.</span>\n } @else if (selectedLocations().length === 0) {\n <span class=\"fs-sm text-light\">Select locations</span>\n } @else {\n @for (loc of visibleSelectedLocations(); track loc.id) {\n <span class=\"inline-flex h-5 items-center gap-2xs rounded bg-primary-50 px-2xs text-primary\">\n <span class=\"fs-xs font-medium\">{{ loc.name }}</span>\n <button\n type=\"button\"\n class=\"inline-flex h-3 w-3 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-primary\"\n [attr.aria-label]=\"'Remove ' + loc.name\"\n (click)=\"$event.stopPropagation(); toggleLocation(loc.id)\"\n >\n <ed-icon name=\"close\" [size]=\"12\" color=\"currentColor\" />\n </button>\n </span>\n }\n @if (hiddenLocationCount() > 0) {\n <span class=\"inline-flex h-5 items-center rounded bg-primary-50 px-2xs fs-xs font-medium text-primary\">\n +{{ hiddenLocationCount() }}\n </span>\n }\n }\n </span>\n <ed-icon name=\"expand_more\" [size]=\"16\" class=\"shrink-0 text-light\" />\n </div>\n @if (locationsMenuOpen()) {\n <ul\n class=\"settings-page__locations-menu absolute bottom-full z-50 m-0 mb-1 max-h-56 w-full list-none overflow-auto rounded-md border border-zinc-300 bg-white p-2xs shadow-lg\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n >\n @for (loc of locations(); track loc.id) {\n <li role=\"option\" [attr.aria-selected]=\"selectedLocationIds().includes(loc.id)\">\n <label class=\"flex cursor-pointer items-center gap-xs rounded px-xs py-2xs fs-sm text-dark hover:bg-lighter\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 accent-blue-600\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n />\n <span class=\"min-w-0 truncate\">{{ loc.name }}</span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n @if (locations().length === 0) {\n <p class=\"m-0 fs-sm text-light\">No locations available.</p>\n }\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Max AI Call Duration<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'maxCallDurationMin')\"\n (ngModelChange)=\"selectNumberField('phone', 'maxCallDurationMin', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of maxCallDurationOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Silence Timeout<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('phone', 'silenceTimeoutSec')\"\n (ngModelChange)=\"selectNumberField('phone', 'silenceTimeoutSec', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of silenceTimeoutOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>After-Hours Behaviour<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"fieldValue('phone', 'afterHoursBehavior')\"\n (ngModelChange)=\"updateField('phone', 'afterHoursBehavior', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of afterHoursOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-md border-t border-light pt-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Minimum lead time (hours)</label>\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n min=\"0\"\n [ngModel]=\"fieldValue('scheduling', 'leadTimeHours')\"\n (ngModelChange)=\"updateField('scheduling', 'leadTimeHours', +$event)\"\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Earliest bookable slot from now.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <ed-icon name=\"calendar_month\" [size]=\"20\" class=\"shrink-0 text-primary\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Allow same-day booking</span>\n <span class=\"fs-xs text-light\">\n Let patients book appointments later today.\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n [attr.aria-pressed]=\"!!fieldValue('scheduling', 'allowSameDay')\"\n aria-label=\"Allow same-day booking\"\n (click)=\"updateField('scheduling', 'allowSameDay', !fieldValue('scheduling', 'allowSameDay'))\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Call Recordings<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'recordingRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'recordingRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Keep Transcripts<span class=\"eds-req\" aria-hidden=\"true\">*</span></label>\n <select\n class=\"form-select is-sm\"\n [ngModel]=\"numberFieldAsString('transcripts', 'transcriptRetainDays')\"\n (ngModelChange)=\"selectNumberField('transcripts', 'transcriptRetainDays', $event)\"\n autocomplete=\"off\"\n >\n @for (opt of retentionOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <div class=\"flex items-center gap-xs\">\n <label class=\"m-0\">Disclosure Warning</label>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n aria-label=\"Disclosure Warning\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"form-textarea min-h-24\"\n rows=\"4\"\n [maxlength]=\"disclosureMax\"\n [ngModel]=\"disclosureText()\"\n (ngModelChange)=\"updateDisclosureText($event)\"\n autocomplete=\"off\"\n ></textarea>\n }\n </div>\n </div>\n } @else if (tab() === 'advanced') {\n <div class=\"flex w-full max-w-3xl flex-col gap-md\">\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-white p-sm\">\n <span class=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary-50 text-primary\" aria-hidden=\"true\">\n <ed-icon name=\"sparkle\" [size]=\"20\" />\n </span>\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">\n @if (toggling()) {\n Updating\u2026\n } @else if (online() === true) {\n Agent online\n } @else if (online() === false) {\n Agent offline\n } @else {\n Status unknown\n }\n </span>\n <span class=\"fs-xs text-light\">\n @if (online() === true) {\n AI receptionist is answering inbound calls.\n } @else if (online() === false) {\n AI is offline \u2014 calls go to the front desk.\n } @else {\n Checking agent status\u2026\n }\n </span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n aria-label=\"Agent online\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <div class=\"flex flex-col gap-xs\">\n <label>Model version</label>\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n <span class=\"fs-xs text-light\">Set by Edvak support.</span>\n </div>\n\n <div class=\"flex items-center gap-sm rounded-md border border-base bg-lighter px-base py-sm opacity-90\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">Debug mode</span>\n <span class=\"fs-xs text-light\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <button\n type=\"button\"\n class=\"settings-page__switch shrink-0\"\n [class.settings-page__switch--on]=\"!!fieldValue('advanced', 'debugMode')\"\n disabled\n [attr.aria-pressed]=\"!!fieldValue('advanced', 'debugMode')\"\n aria-label=\"Debug mode\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n </div>\n } @else {\n <div class=\"grid grid-cols-2 gap-x-md gap-y-md below-md:grid-cols-1\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"col-span-full flex cursor-pointer items-center justify-between gap-base rounded-md border border-base bg-lighter px-base py-sm\"\n [class.cursor-default]=\"fieldIsReadOnly(tab(), field.key)\"\n [class.opacity-90]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"flex min-w-0 flex-col gap-2xs\">\n <span class=\"fs-sm text-dark\">{{ field.label }}</span>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 shrink-0 accent-blue-600 disabled:cursor-not-allowed\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n </label>\n } @else if (field.type === 'select' && field.options?.length) {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n <select\n class=\"form-select is-sm\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n >\n @for (opt of field.options; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n </div>\n } @else {\n <div class=\"flex flex-col gap-xs\">\n <label>{{ field.label }}</label>\n @if (field.description) {\n <span\n class=\"fs-sm text-light\"\n [class.text-lighter]=\"fieldIsReadOnly(tab(), field.key)\"\n >{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n class=\"form-input is-sm\"\n type=\"number\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, +$event)\"\n autocomplete=\"off\"\n />\n } @else {\n <input\n class=\"form-input is-sm\"\n type=\"text\"\n [disabled]=\"fieldIsReadOnly(tab(), field.key)\"\n [ngModel]=\"fieldValue(tab(), field.key)\"\n (ngModelChange)=\"updateField(tab(), field.key, $event)\"\n autocomplete=\"off\"\n />\n }\n </div>\n }\n }\n </div>\n }\n</ng-template>\n\n<eds-knowledge-test-modal\n [open]=\"knowledgeTestOpen()\"\n [whatToSay]=\"whatToSay()\"\n [whatNotToSay]=\"whatNotToSay()\"\n [voiceId]=\"selectedVoiceId()\"\n (closed)=\"closeTestCall()\"\n/>\n\n@if (confirmOffOpen()) {\n <app-modal\n title=\"Turn off AI receptionist?\"\n subtitle=\"Inbound callers will be transferred to the front desk instead of the AI.\"\n maxWidth=\"480px\"\n (closed)=\"cancelConfirmOff()\"\n >\n <p class=\"mb-4 m-0 text-sm leading-normal text-zinc-500\">While offline, the AI will not answer calls. Callers are routed to the human receptionist.</p>\n <div class=\"flex justify-end gap-2\">\n <ed-button variant=\"secondary\" outline size=\"sm\" (click$)=\"cancelConfirmOff()\">Cancel</ed-button>\n <ed-button variant=\"danger\" fill size=\"sm\" [disabled]=\"toggling()\" (click$)=\"confirmTurnOff()\">\n Turn off AI receptionist\n </ed-button>\n </div>\n </app-modal>\n}\n", styles: [":host{display:block}.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)}.eds-req{display:inline;margin-left:.2em;color:#e10d0d;font-size:.85em;font-weight:600;line-height:1;vertical-align:text-top;position:static}.eds-req:after{content:none!important}.form-input:disabled,.form-select:disabled{color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f1f5f9);border-color:var(--eds-color-border, #e2e8f0);cursor:not-allowed;opacity:1}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}}\n"] }]
|
|
5476
5476
|
}] });
|
|
5477
5477
|
|
|
5478
5478
|
var settingsPage_component = /*#__PURE__*/Object.freeze({
|
|
@@ -6346,7 +6346,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6346
6346
|
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
6347
6347
|
{
|
|
6348
6348
|
path: 'dashboard',
|
|
6349
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-
|
|
6349
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-dashboard-page.component-CcphzJ4N.mjs').then((m) => m.DashboardPageComponent),
|
|
6350
6350
|
},
|
|
6351
6351
|
{
|
|
6352
6352
|
path: 'live',
|
|
@@ -6355,37 +6355,37 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6355
6355
|
{
|
|
6356
6356
|
path: '',
|
|
6357
6357
|
pathMatch: 'full',
|
|
6358
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-
|
|
6358
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-calls-page.component-gTy7IynR.mjs').then((m) => m.LiveCallsPageComponent),
|
|
6359
6359
|
},
|
|
6360
6360
|
{
|
|
6361
6361
|
path: ':id',
|
|
6362
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-
|
|
6362
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-live-monitor-page.component-C-sDCE3S.mjs').then((m) => m.LiveMonitorPageComponent),
|
|
6363
6363
|
},
|
|
6364
6364
|
],
|
|
6365
6365
|
},
|
|
6366
6366
|
{
|
|
6367
6367
|
path: 'history',
|
|
6368
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-
|
|
6368
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-page.component-krL6GnMf.mjs').then((m) => m.CallHistoryPageComponent),
|
|
6369
6369
|
},
|
|
6370
6370
|
{
|
|
6371
6371
|
path: 'history/:id',
|
|
6372
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-
|
|
6372
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-call-history-detail-page.component-D3GAw7pk.mjs').then((m) => m.CallHistoryDetailPageComponent),
|
|
6373
6373
|
},
|
|
6374
6374
|
{
|
|
6375
6375
|
path: 'activities',
|
|
6376
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-
|
|
6376
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-activities-page.component-D16UQWsH.mjs').then((m) => m.ActivitiesPageComponent),
|
|
6377
6377
|
},
|
|
6378
6378
|
{
|
|
6379
6379
|
path: 'tasks',
|
|
6380
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-
|
|
6380
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-tasks-page.component-OP77Shm4.mjs').then((m) => m.TasksPageComponent),
|
|
6381
6381
|
},
|
|
6382
6382
|
{
|
|
6383
6383
|
path: 'analytics',
|
|
6384
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-
|
|
6384
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-analytics-page.component-DEXdD1mB.mjs').then((m) => m.AnalyticsPageComponent),
|
|
6385
6385
|
},
|
|
6386
6386
|
{
|
|
6387
6387
|
path: 'audit',
|
|
6388
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-
|
|
6388
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-audit-page.component-D-uAGYwd.mjs').then((m) => m.AuditPageComponent),
|
|
6389
6389
|
},
|
|
6390
6390
|
{
|
|
6391
6391
|
path: 'settings',
|
|
@@ -6393,7 +6393,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6393
6393
|
},
|
|
6394
6394
|
{
|
|
6395
6395
|
path: 'settings/outbound-purposes',
|
|
6396
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-
|
|
6396
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-outbound-purposes-page.component-87FxvbsK.mjs').then((m) => m.OutboundPurposesPageComponent),
|
|
6397
6397
|
},
|
|
6398
6398
|
{
|
|
6399
6399
|
path: 'knowledge',
|
|
@@ -6402,7 +6402,7 @@ const VOICE_RECEPTIONIST_FEATURE_ROUTES = [
|
|
|
6402
6402
|
},
|
|
6403
6403
|
{
|
|
6404
6404
|
path: 'wizard',
|
|
6405
|
-
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-
|
|
6405
|
+
loadComponent: () => import('./edvak99-voice-receptionist-widget-wizard-page.component-CefGWtVL.mjs').then((m) => m.WizardPageComponent),
|
|
6406
6406
|
},
|
|
6407
6407
|
];
|
|
6408
6408
|
/**
|
|
@@ -6464,4 +6464,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
6464
6464
|
*/
|
|
6465
6465
|
|
|
6466
6466
|
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 };
|
|
6467
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-
|
|
6467
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CGbyYMbi.mjs.map
|