@edvak99/voice-receptionist-widget 1.1.3 → 1.1.4
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-DT9UKvQO.mjs +307 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs} +1275 -561
- package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +278 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +418 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +194 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-B8_qp7Fe.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs} +3 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-B8_qp7Fe.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map} +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cj2XTeK4.mjs → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs} +2 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cj2XTeK4.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs +21 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +440 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-eds-elapsed.pipe-BgLsnq6A.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs} +5 -21
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs +38 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +27 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs +22 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-list.service-0kGWO6sh.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs} +15 -5
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +203 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +81 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +1070 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +48 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs +127 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +268 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +968 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-DXxjMVUv.mjs → edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs} +56 -5
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C1Z6yC0q.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs} +4 -2
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-C1Z6yC0q.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +9 -6
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +8 -8
- package/lib/components/knowledge-test-modal/knowledge-test-modal.component.d.ts +28 -0
- package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +7 -4
- package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +26 -0
- package/lib/layout/eds-module-nav.component.d.ts +0 -3
- package/lib/mocks/agent-activity.seed.d.ts +2 -0
- package/lib/models/agent-activity.types.d.ts +54 -0
- package/lib/models/ai-receptionist.types.d.ts +25 -0
- package/lib/models/api-dto.types.d.ts +18 -0
- package/lib/models/live-transcript.types.d.ts +2 -1
- package/lib/models/outbound-patient.types.d.ts +23 -0
- package/lib/models/outbound-purpose.types.d.ts +2 -0
- package/lib/models/staff-media.types.d.ts +36 -0
- package/lib/pages/activities-page/activities-page.component.d.ts +57 -0
- package/lib/pages/analytics-page/analytics-page.component.d.ts +38 -14
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +2 -3
- package/lib/pages/call-history-page/call-history-page.component.d.ts +9 -6
- package/lib/pages/dashboard-page/dashboard-page.component.d.ts +12 -8
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +14 -24
- package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +34 -1
- package/lib/pages/settings-page/settings-fields.config.d.ts +16 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +48 -14
- package/lib/services/ai-receptionist-api.mock.d.ts +34 -0
- package/lib/services/ai-receptionist-api.service.d.ts +56 -1
- package/lib/services/live-transcript.service.d.ts +1 -0
- package/lib/services/staff-media.service.d.ts +52 -0
- package/lib/ui/eds-avatar.component.d.ts +5 -2
- package/lib/ui/eds-button.component.d.ts +1 -1
- package/lib/utils/call-history-display.util.d.ts +21 -0
- package/lib/utils/outbound-purpose.util.d.ts +19 -0
- package/lib/utils/transcript-timing.util.d.ts +1 -1
- package/lib/utils/welcome-message.util.d.ts +1 -1
- package/package.json +2 -1
- package/styles/eds.scss +95 -66
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Dsujn5az.mjs +0 -113
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Dsujn5az.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-xoLlHj8K.mjs +0 -67
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-xoLlHj8K.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-2K9W77As.mjs +0 -316
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-2K9W77As.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B5AnEjoh.mjs +0 -174
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-B5AnEjoh.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-BgsBCKgZ.mjs +0 -70
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-BgsBCKgZ.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-BNdnbAVM.mjs +0 -248
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-BNdnbAVM.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CbphhP6o.mjs +0 -27
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CbphhP6o.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BgLsnq6A.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs +0 -609
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-0kGWO6sh.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DfLmK89M.mjs +0 -115
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DfLmK89M.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DNpLFqBw.mjs +0 -574
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DNpLFqBw.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +0 -54
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CYSZ832p.mjs +0 -265
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CYSZ832p.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-B9nv6wtB.mjs +0 -509
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-B9nv6wtB.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +0 -52
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DXxjMVUv.mjs.map +0 -1
- package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +0 -22
- package/lib/knowledge/components/faq-form/faq-form.component.d.ts +0 -27
- package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +0 -17
- package/lib/knowledge/mocks/practice-faq.seed.d.ts +0 -2
- package/lib/knowledge/models/practice-faq.model.d.ts +0 -50
- package/lib/knowledge/pages/knowledge-hub.page.d.ts +0 -54
- package/lib/knowledge/services/practice-faq-api.mock.d.ts +0 -16
- package/lib/knowledge/services/practice-faq-api.service.d.ts +0 -26
- package/lib/knowledge/utils/faq.util.d.ts +0 -3
- package/lib/layout/eds-topbar.component.d.ts +0 -29
- package/lib/pages/audit-page/audit-page.component.d.ts +0 -21
- package/lib/ui/eds-waveform.component.d.ts +0 -8
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, EventEmitter, signal, Output, Input, ChangeDetectionStrategy, Component, Injectable, computed } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/forms';
|
|
4
|
+
import { FormsModule } from '@angular/forms';
|
|
5
|
+
import { ActivatedRoute } from '@angular/router';
|
|
6
|
+
import { A as AiReceptionistApiService, W as WELCOME_MESSAGE_MAX, n as buildAutoWelcomeTemplate, o as resolveWelcomePreview } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs';
|
|
7
|
+
import { E as EdsButtonComponent } from './edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs';
|
|
8
|
+
import { T as ToastService, E as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs';
|
|
9
|
+
import { M as ModalComponent } from './edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs';
|
|
10
|
+
import { switchMap, catchError, of, tap, throwError, finalize } from 'rxjs';
|
|
11
|
+
|
|
12
|
+
const QUESTION_MAX = 500;
|
|
13
|
+
class KnowledgeTestModalComponent {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.api = inject(AiReceptionistApiService);
|
|
16
|
+
this.toast = inject(ToastService);
|
|
17
|
+
this.open = false;
|
|
18
|
+
this.whatToSay = '';
|
|
19
|
+
this.whatNotToSay = '';
|
|
20
|
+
this.voiceId = '';
|
|
21
|
+
this.closed = new EventEmitter();
|
|
22
|
+
this.questionMax = QUESTION_MAX;
|
|
23
|
+
this.question = signal('');
|
|
24
|
+
this.testing = signal(false);
|
|
25
|
+
this.answer = signal(null);
|
|
26
|
+
this.playing = signal(false);
|
|
27
|
+
this.audio = null;
|
|
28
|
+
this.objectUrl = null;
|
|
29
|
+
}
|
|
30
|
+
ngOnChanges(changes) {
|
|
31
|
+
if (changes['open'] && this.open) {
|
|
32
|
+
this.question.set('');
|
|
33
|
+
this.answer.set(null);
|
|
34
|
+
this.stopAudio();
|
|
35
|
+
}
|
|
36
|
+
if (changes['open'] && !this.open) {
|
|
37
|
+
this.stopAudio();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ngOnDestroy() {
|
|
41
|
+
this.stopAudio();
|
|
42
|
+
}
|
|
43
|
+
close() {
|
|
44
|
+
this.stopAudio();
|
|
45
|
+
this.closed.emit();
|
|
46
|
+
}
|
|
47
|
+
onQuestionChange(value) {
|
|
48
|
+
this.question.set(value.slice(0, QUESTION_MAX));
|
|
49
|
+
}
|
|
50
|
+
runTest() {
|
|
51
|
+
const q = this.question().trim();
|
|
52
|
+
if (!q || this.testing())
|
|
53
|
+
return;
|
|
54
|
+
const whatToSay = this.whatToSay.trim();
|
|
55
|
+
const whatNotToSay = this.whatNotToSay.trim();
|
|
56
|
+
if (!whatToSay || !whatNotToSay) {
|
|
57
|
+
this.toast.show('Fill in both “What to say” and “What not to say” before testing.', 'error');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.testing.set(true);
|
|
61
|
+
this.answer.set(null);
|
|
62
|
+
this.stopAudio();
|
|
63
|
+
this.api
|
|
64
|
+
.testKnowledgeAnswer({
|
|
65
|
+
question: q,
|
|
66
|
+
whatToSay,
|
|
67
|
+
whatNotToSay,
|
|
68
|
+
voiceId: this.voiceId.trim() || undefined,
|
|
69
|
+
})
|
|
70
|
+
.subscribe({
|
|
71
|
+
next: (result) => {
|
|
72
|
+
this.testing.set(false);
|
|
73
|
+
this.answer.set(result.answer);
|
|
74
|
+
this.playAudio(result.audioBlob);
|
|
75
|
+
},
|
|
76
|
+
error: (err) => {
|
|
77
|
+
this.testing.set(false);
|
|
78
|
+
const message = err instanceof Error && err.message.trim()
|
|
79
|
+
? err.message
|
|
80
|
+
: 'Could not generate a test answer.';
|
|
81
|
+
this.toast.show(message, 'error');
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
replay() {
|
|
86
|
+
if (!this.audio)
|
|
87
|
+
return;
|
|
88
|
+
this.audio.currentTime = 0;
|
|
89
|
+
this.playing.set(true);
|
|
90
|
+
void this.audio.play().catch(() => {
|
|
91
|
+
this.playing.set(false);
|
|
92
|
+
this.toast.show('Could not play audio in this browser.', 'error');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
playAudio(blob) {
|
|
96
|
+
this.stopAudio();
|
|
97
|
+
this.objectUrl = URL.createObjectURL(blob);
|
|
98
|
+
const audio = new Audio(this.objectUrl);
|
|
99
|
+
this.audio = audio;
|
|
100
|
+
this.playing.set(true);
|
|
101
|
+
audio.onended = () => this.playing.set(false);
|
|
102
|
+
audio.onerror = () => {
|
|
103
|
+
this.playing.set(false);
|
|
104
|
+
this.toast.show('Could not play audio in this browser.', 'error');
|
|
105
|
+
};
|
|
106
|
+
void audio.play().catch(() => {
|
|
107
|
+
this.playing.set(false);
|
|
108
|
+
this.toast.show('Could not play audio in this browser.', 'error');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
stopAudio() {
|
|
112
|
+
if (this.audio) {
|
|
113
|
+
this.audio.pause();
|
|
114
|
+
this.audio.onended = null;
|
|
115
|
+
this.audio.onerror = null;
|
|
116
|
+
this.audio = null;
|
|
117
|
+
}
|
|
118
|
+
if (this.objectUrl) {
|
|
119
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
120
|
+
this.objectUrl = null;
|
|
121
|
+
}
|
|
122
|
+
this.playing.set(false);
|
|
123
|
+
}
|
|
124
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeTestModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
125
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: KnowledgeTestModalComponent, isStandalone: true, selector: "eds-knowledge-test-modal", inputs: { open: "open", whatToSay: "whatToSay", whatNotToSay: "whatNotToSay", voiceId: "voiceId" }, outputs: { closed: "closed" }, usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form class=\"kb-test\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <label class=\"kb-test__field\">\n <span class=\"kb-test__label\">\n What might a caller ask? <span class=\"kb-test__req\">*</span>\n </span>\n <textarea\n class=\"kb-test__textarea\"\n rows=\"3\"\n [maxlength]=\"questionMax\"\n [ngModel]=\"question()\"\n (ngModelChange)=\"onQuestionChange($event)\"\n name=\"question\"\n placeholder=\"e.g. What are your Saturday hours? Is parking free?\"\n [disabled]=\"testing()\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"kb-test__hint\">{{ question().length }}/{{ questionMax }}</span>\n </label>\n\n @if (answer(); as spoken) {\n <div class=\"kb-test__answer\" role=\"status\">\n <div class=\"kb-test__answer-head\">\n <eds-icon name=\"graphic_eq\" [size]=\"18\" />\n <span>Agent reply</span>\n @if (playing()) {\n <span class=\"kb-test__playing\">Playing\u2026</span>\n } @else {\n <button type=\"button\" class=\"kb-test__replay\" (click)=\"replay()\">\n Replay\n </button>\n }\n </div>\n <p class=\"kb-test__answer-text\">{{ spoken }}</p>\n </div>\n }\n\n <footer class=\"kb-test__actions\">\n <eds-button type=\"button\" variant=\"secondary\" (click)=\"close()\" [disabled]=\"testing()\">\n Close\n </eds-button>\n <eds-button type=\"submit\" variant=\"primary\" [disabled]=\"testing() || !question().trim()\">\n @if (testing()) {\n Generating\u2026\n } @else {\n <eds-icon name=\"play_arrow\" [size]=\"18\" />\n Test\n }\n </eds-button>\n </footer>\n </form>\n </app-modal>\n}\n", styles: [".kb-test{display:flex;flex-direction:column;gap:16px}.kb-test__field{display:flex;flex-direction:column;gap:6px}.kb-test__label{font-size:13px;font-weight:600;color:var(--eds-color-text, #0f172a)}.kb-test__req{color:#dc2626}.kb-test__textarea{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:8px;font:inherit;font-size:14px;line-height:1.45;resize:vertical;min-height:84px;background:#fff}.kb-test__textarea:focus{outline:2px solid color-mix(in srgb,var(--eds-primary, #2563eb) 35%,transparent);outline-offset:1px;border-color:var(--eds-primary, #2563eb)}.kb-test__textarea:disabled{opacity:.7;background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__hint{font-size:12px;color:var(--eds-color-text-muted, #64748b);align-self:flex-end}.kb-test__answer{padding:12px 14px;border-radius:8px;border:1px solid var(--eds-color-border, #e2e8f0);background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__answer-head{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--eds-color-text-muted, #64748b);margin-bottom:8px}.kb-test__playing{margin-left:auto;font-weight:500;text-transform:none;letter-spacing:0;color:var(--eds-primary, #2563eb)}.kb-test__replay{margin-left:auto;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;font-weight:600;text-transform:none;letter-spacing:0;cursor:pointer;padding:2px 4px}.kb-test__answer-text{margin:0;font-size:14px;line-height:1.55;color:var(--eds-color-text, #0f172a)}.kb-test__actions{display:flex;justify-content:flex-end;gap:10px;padding-top:4px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
126
|
+
}
|
|
127
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeTestModalComponent, decorators: [{
|
|
128
|
+
type: Component,
|
|
129
|
+
args: [{ selector: 'eds-knowledge-test-modal', imports: [FormsModule, ModalComponent, EdsButtonComponent, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <app-modal\n title=\"Test Knowledge Base\"\n subtitle=\"Ask a sample caller question \u2014 hear how the agent answers with your guidance and voice\"\n maxWidth=\"560px\"\n (closed)=\"close()\"\n >\n <form class=\"kb-test\" (ngSubmit)=\"runTest()\" novalidate autocomplete=\"off\">\n <label class=\"kb-test__field\">\n <span class=\"kb-test__label\">\n What might a caller ask? <span class=\"kb-test__req\">*</span>\n </span>\n <textarea\n class=\"kb-test__textarea\"\n rows=\"3\"\n [maxlength]=\"questionMax\"\n [ngModel]=\"question()\"\n (ngModelChange)=\"onQuestionChange($event)\"\n name=\"question\"\n placeholder=\"e.g. What are your Saturday hours? Is parking free?\"\n [disabled]=\"testing()\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"kb-test__hint\">{{ question().length }}/{{ questionMax }}</span>\n </label>\n\n @if (answer(); as spoken) {\n <div class=\"kb-test__answer\" role=\"status\">\n <div class=\"kb-test__answer-head\">\n <eds-icon name=\"graphic_eq\" [size]=\"18\" />\n <span>Agent reply</span>\n @if (playing()) {\n <span class=\"kb-test__playing\">Playing\u2026</span>\n } @else {\n <button type=\"button\" class=\"kb-test__replay\" (click)=\"replay()\">\n Replay\n </button>\n }\n </div>\n <p class=\"kb-test__answer-text\">{{ spoken }}</p>\n </div>\n }\n\n <footer class=\"kb-test__actions\">\n <eds-button type=\"button\" variant=\"secondary\" (click)=\"close()\" [disabled]=\"testing()\">\n Close\n </eds-button>\n <eds-button type=\"submit\" variant=\"primary\" [disabled]=\"testing() || !question().trim()\">\n @if (testing()) {\n Generating\u2026\n } @else {\n <eds-icon name=\"play_arrow\" [size]=\"18\" />\n Test\n }\n </eds-button>\n </footer>\n </form>\n </app-modal>\n}\n", styles: [".kb-test{display:flex;flex-direction:column;gap:16px}.kb-test__field{display:flex;flex-direction:column;gap:6px}.kb-test__label{font-size:13px;font-weight:600;color:var(--eds-color-text, #0f172a)}.kb-test__req{color:#dc2626}.kb-test__textarea{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:8px;font:inherit;font-size:14px;line-height:1.45;resize:vertical;min-height:84px;background:#fff}.kb-test__textarea:focus{outline:2px solid color-mix(in srgb,var(--eds-primary, #2563eb) 35%,transparent);outline-offset:1px;border-color:var(--eds-primary, #2563eb)}.kb-test__textarea:disabled{opacity:.7;background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__hint{font-size:12px;color:var(--eds-color-text-muted, #64748b);align-self:flex-end}.kb-test__answer{padding:12px 14px;border-radius:8px;border:1px solid var(--eds-color-border, #e2e8f0);background:var(--eds-color-surface-alt, #f8fafc)}.kb-test__answer-head{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--eds-color-text-muted, #64748b);margin-bottom:8px}.kb-test__playing{margin-left:auto;font-weight:500;text-transform:none;letter-spacing:0;color:var(--eds-primary, #2563eb)}.kb-test__replay{margin-left:auto;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;font-weight:600;text-transform:none;letter-spacing:0;cursor:pointer;padding:2px 4px}.kb-test__answer-text{margin:0;font-size:14px;line-height:1.55;color:var(--eds-color-text, #0f172a)}.kb-test__actions{display:flex;justify-content:flex-end;gap:10px;padding-top:4px}\n"] }]
|
|
130
|
+
}], propDecorators: { open: [{
|
|
131
|
+
type: Input,
|
|
132
|
+
args: [{ required: true }]
|
|
133
|
+
}], whatToSay: [{
|
|
134
|
+
type: Input
|
|
135
|
+
}], whatNotToSay: [{
|
|
136
|
+
type: Input
|
|
137
|
+
}], voiceId: [{
|
|
138
|
+
type: Input
|
|
139
|
+
}], closed: [{
|
|
140
|
+
type: Output
|
|
141
|
+
}] } });
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Shared source of truth for Ava online/offline across topbar + module nav.
|
|
145
|
+
* Always re-fetches GET /status after mutations.
|
|
146
|
+
*/
|
|
147
|
+
class AgentOnlineService {
|
|
148
|
+
constructor() {
|
|
149
|
+
this.api = inject(AiReceptionistApiService);
|
|
150
|
+
/** true = online, false = offline, null = unknown / not loaded */
|
|
151
|
+
this._online = signal(null);
|
|
152
|
+
this._loading = signal(false);
|
|
153
|
+
this._toggling = signal(false);
|
|
154
|
+
this._error = signal(null);
|
|
155
|
+
this._readiness = signal(null);
|
|
156
|
+
this.online = this._online.asReadonly();
|
|
157
|
+
this.loading = this._loading.asReadonly();
|
|
158
|
+
this.toggling = this._toggling.asReadonly();
|
|
159
|
+
this.error = this._error.asReadonly();
|
|
160
|
+
this.readiness = this._readiness.asReadonly();
|
|
161
|
+
}
|
|
162
|
+
refresh() {
|
|
163
|
+
this._loading.set(true);
|
|
164
|
+
this._error.set(null);
|
|
165
|
+
this.api.getStatus().subscribe({
|
|
166
|
+
next: ({ data }) => {
|
|
167
|
+
this._online.set(data.agentOn);
|
|
168
|
+
this._readiness.set(data.readiness);
|
|
169
|
+
this._loading.set(false);
|
|
170
|
+
},
|
|
171
|
+
error: (err) => {
|
|
172
|
+
this._online.set(null);
|
|
173
|
+
this._error.set(err.message ?? 'Could not load agent status.');
|
|
174
|
+
this._loading.set(false);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* PUT /agent-online then re-fetch GET /status.
|
|
180
|
+
*/
|
|
181
|
+
setOnline(enabled) {
|
|
182
|
+
this._toggling.set(true);
|
|
183
|
+
this._error.set(null);
|
|
184
|
+
return this.api.setAgentOnline(enabled).pipe(switchMap(() => this.api.getStatus().pipe(catchError((err) => {
|
|
185
|
+
this._error.set(err.message ?? 'Status refresh failed after toggle.');
|
|
186
|
+
return of(null);
|
|
187
|
+
}))), tap((result) => {
|
|
188
|
+
if (result) {
|
|
189
|
+
this._online.set(result.data.agentOn);
|
|
190
|
+
this._readiness.set(result.data.readiness);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Toggle likely succeeded; use requested value until refresh works.
|
|
194
|
+
this._online.set(enabled);
|
|
195
|
+
}
|
|
196
|
+
}), catchError((err) => {
|
|
197
|
+
this._error.set(err.message ?? 'Failed to update agent online status.');
|
|
198
|
+
return throwError(() => err);
|
|
199
|
+
}), finalize(() => this._toggling.set(false)));
|
|
200
|
+
}
|
|
201
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
202
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, providedIn: 'root' }); }
|
|
203
|
+
}
|
|
204
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AgentOnlineService, decorators: [{
|
|
205
|
+
type: Injectable,
|
|
206
|
+
args: [{ providedIn: 'root' }]
|
|
207
|
+
}] });
|
|
208
|
+
|
|
209
|
+
/** Curated ElevenLabs voices — keep in sync with backend `tts-voice-presets.ts`. */
|
|
210
|
+
const DEFAULT_TTS_VOICE_ID = '7EzWGsX10sAS4c9m9cPf';
|
|
211
|
+
const TTS_VOICE_PRESETS = [
|
|
212
|
+
{
|
|
213
|
+
id: '7EzWGsX10sAS4c9m9cPf',
|
|
214
|
+
label: 'Jack John – Conversational and Upbeat',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'NUjosfEayZAdRcDmcHM8',
|
|
218
|
+
label: 'Madison – Cool, Calm & Conversational',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'gE0owC0H9C8SzfDyIUtB',
|
|
222
|
+
label: 'Ivanna – Sassy, Condescending and Clear',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'Uc7anshoV8mdBhDnEZEX',
|
|
226
|
+
label: 'Cecily – Clear and Warm',
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const AGENT_TONE_OPTIONS = [
|
|
231
|
+
{ value: 'warm_friendly', label: 'Warm & friendly' },
|
|
232
|
+
{ value: 'professional', label: 'Professional' },
|
|
233
|
+
{ value: 'efficient_brief', label: 'Efficient & brief' },
|
|
234
|
+
{ value: 'casual_friendly', label: 'Casual & friendly' },
|
|
235
|
+
];
|
|
236
|
+
const LANGUAGE_OPTIONS = [
|
|
237
|
+
{ value: 'English', label: 'English' },
|
|
238
|
+
{ value: 'Spanish', label: 'Spanish' },
|
|
239
|
+
{ value: 'Chinese', label: 'Chinese' },
|
|
240
|
+
{ value: 'Hindi', label: 'Hindi' },
|
|
241
|
+
{ value: 'French', label: 'French' },
|
|
242
|
+
{ value: 'German', label: 'German' },
|
|
243
|
+
{ value: 'Portuguese', label: 'Portuguese' },
|
|
244
|
+
{ value: 'Arabic', label: 'Arabic' },
|
|
245
|
+
{ value: 'Korean', label: 'Korean' },
|
|
246
|
+
{ value: 'Japanese', label: 'Japanese' },
|
|
247
|
+
];
|
|
248
|
+
const GREETING_VARIABLES = [
|
|
249
|
+
{ key: 'practiceName', label: 'Practice Name', token: '{{practiceName}}' },
|
|
250
|
+
{ key: 'agentName', label: 'Agent Name', token: '{{agentName}}' },
|
|
251
|
+
{ key: 'locationName', label: 'Location Name', token: '{{locationName}}' },
|
|
252
|
+
{ key: 'businessHours', label: 'Business Hours', token: '{{businessHours}}' },
|
|
253
|
+
];
|
|
254
|
+
const RETENTION_DAY_OPTIONS = [
|
|
255
|
+
{ value: '7', label: '7 days' },
|
|
256
|
+
{ value: '14', label: '14 days' },
|
|
257
|
+
{ value: '30', label: '30 days' },
|
|
258
|
+
{ value: '60', label: '60 days' },
|
|
259
|
+
{ value: '90', label: '90 days' },
|
|
260
|
+
{ value: '180', label: '180 days' },
|
|
261
|
+
{ value: '365', label: '1 year' },
|
|
262
|
+
];
|
|
263
|
+
const RING_TIMEOUT_OPTIONS = [
|
|
264
|
+
{ value: '10', label: '10 sec' },
|
|
265
|
+
{ value: '15', label: '15 sec' },
|
|
266
|
+
{ value: '20', label: '20 sec' },
|
|
267
|
+
{ value: '30', label: '30 sec' },
|
|
268
|
+
{ value: '45', label: '45 sec' },
|
|
269
|
+
{ value: '60', label: '60 sec' },
|
|
270
|
+
];
|
|
271
|
+
const MAX_CALL_DURATION_OPTIONS = [
|
|
272
|
+
{ value: '3', label: '3 min' },
|
|
273
|
+
{ value: '5', label: '5 min' },
|
|
274
|
+
{ value: '10', label: '10 min' },
|
|
275
|
+
{ value: '15', label: '15 min' },
|
|
276
|
+
{ value: '20', label: '20 min' },
|
|
277
|
+
{ value: '30', label: '30 min' },
|
|
278
|
+
];
|
|
279
|
+
const SILENCE_TIMEOUT_OPTIONS = [
|
|
280
|
+
{ value: '10', label: '10 seconds' },
|
|
281
|
+
{ value: '15', label: '15 seconds' },
|
|
282
|
+
{ value: '20', label: '20 seconds' },
|
|
283
|
+
{ value: '30', label: '30 seconds' },
|
|
284
|
+
{ value: '45', label: '45 seconds' },
|
|
285
|
+
{ value: '60', label: '60 seconds' },
|
|
286
|
+
];
|
|
287
|
+
const AI_ANSWER_MODE_OPTIONS = [
|
|
288
|
+
{ value: 'always_first', label: 'Always first' },
|
|
289
|
+
{ value: 'ring_then_ai', label: 'Ring staff, then AI' },
|
|
290
|
+
{ value: 'ai_backup', label: 'AI as backup only' },
|
|
291
|
+
];
|
|
292
|
+
const AFTER_HOURS_OPTIONS = [
|
|
293
|
+
{ value: 'ai_answers', label: 'AI answers' },
|
|
294
|
+
{ value: 'voicemail', label: 'Voicemail' },
|
|
295
|
+
{ value: 'transfer', label: 'Transfer to after-hours number' },
|
|
296
|
+
];
|
|
297
|
+
const DEFAULT_DISCLOSURE_WARNING = 'This call may be handled by an automated assistant and could be recorded for quality and training. You can ask to speak with a staff member at any time.';
|
|
298
|
+
const SETTINGS_SECTION_META = {
|
|
299
|
+
general: {
|
|
300
|
+
title: 'AI Identity',
|
|
301
|
+
description: 'Name, voice, languages, tone, and inbound greeting.',
|
|
302
|
+
},
|
|
303
|
+
phone: {
|
|
304
|
+
title: 'Phone & Availability',
|
|
305
|
+
description: 'Inbound routing, ring timing, locations, and after-hours behavior.',
|
|
306
|
+
},
|
|
307
|
+
tasks: {
|
|
308
|
+
title: 'Task Rules',
|
|
309
|
+
description: 'When the AI should create staff tasks after a call.',
|
|
310
|
+
},
|
|
311
|
+
scheduling: {
|
|
312
|
+
title: 'Scheduling Rules',
|
|
313
|
+
description: 'Rules for booking and rescheduling appointments.',
|
|
314
|
+
},
|
|
315
|
+
refill: {
|
|
316
|
+
title: 'Medication refills',
|
|
317
|
+
description: 'Refill approval windows and staff review requirements.',
|
|
318
|
+
},
|
|
319
|
+
verify: {
|
|
320
|
+
title: 'Patient Verification',
|
|
321
|
+
description: 'Identity checks before accessing chart or scheduling data.',
|
|
322
|
+
},
|
|
323
|
+
escalation: {
|
|
324
|
+
title: 'Safety Escalation',
|
|
325
|
+
description: 'When to offer a human or transfer to reception.',
|
|
326
|
+
},
|
|
327
|
+
outbound: {
|
|
328
|
+
title: 'Outbound Purpose',
|
|
329
|
+
description: 'Outbound dialer defaults. Purpose templates are managed separately.',
|
|
330
|
+
},
|
|
331
|
+
knowledge: {
|
|
332
|
+
title: 'Knowledge Base',
|
|
333
|
+
description: 'Practice information the AI should share — and topics it must avoid.',
|
|
334
|
+
},
|
|
335
|
+
transcripts: {
|
|
336
|
+
title: 'Data & Compliance',
|
|
337
|
+
description: 'Recording retention, transcripts, and disclosure warnings.',
|
|
338
|
+
},
|
|
339
|
+
advanced: {
|
|
340
|
+
title: 'Advanced',
|
|
341
|
+
description: 'Agent online status and model options.',
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
/** Entire sections where every field is read-only (including unknown API fields). */
|
|
345
|
+
const READ_ONLY_SECTIONS = new Set(['verify']);
|
|
346
|
+
/** Settings tabs that stay visible but cannot be selected/edited. */
|
|
347
|
+
const DISABLED_SETTINGS_TABS = new Set(['verify', 'escalation']);
|
|
348
|
+
/** Individual fields locked for compliance, telephony, or core agent identity. */
|
|
349
|
+
const READ_ONLY_FIELD_KEYS = [
|
|
350
|
+
{ section: 'general', key: 'timezone', reason: 'Practice timezone' },
|
|
351
|
+
{ section: 'phone', key: 'mainNumber', reason: 'AI phone number' },
|
|
352
|
+
{ section: 'phone', key: 'transferNumber', reason: 'Transfer number' },
|
|
353
|
+
{ section: 'tasks', key: 'defaultAssignee', reason: 'Default task assignee' },
|
|
354
|
+
{ section: 'outbound', key: 'callerId', reason: 'Outbound caller ID' },
|
|
355
|
+
{ section: 'advanced', key: 'modelVersion', reason: 'Voice AI model profile' },
|
|
356
|
+
{ section: 'advanced', key: 'debugMode', reason: 'Debug logging' },
|
|
357
|
+
];
|
|
358
|
+
const READ_ONLY_LOOKUP = new Set(READ_ONLY_FIELD_KEYS.map(({ section, key }) => `${section}.${key}`));
|
|
359
|
+
function isFieldReadOnly(section, key) {
|
|
360
|
+
if (READ_ONLY_SECTIONS.has(section))
|
|
361
|
+
return true;
|
|
362
|
+
return READ_ONLY_LOOKUP.has(`${section}.${key}`);
|
|
363
|
+
}
|
|
364
|
+
function readOnlyFieldReason(section, key) {
|
|
365
|
+
return READ_ONLY_FIELD_KEYS.find((f) => f.section === section && f.key === key)?.reason;
|
|
366
|
+
}
|
|
367
|
+
const SETTINGS_FIELDS = {
|
|
368
|
+
general: [
|
|
369
|
+
{
|
|
370
|
+
key: 'agentName',
|
|
371
|
+
label: 'Name',
|
|
372
|
+
description: 'How the assistant introduces itself.',
|
|
373
|
+
type: 'text',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
key: 'ttsVoiceId',
|
|
377
|
+
label: 'Voice',
|
|
378
|
+
description: 'Spoken voice used on phone calls.',
|
|
379
|
+
type: 'select',
|
|
380
|
+
options: TTS_VOICE_PRESETS.map((p) => ({ value: p.id, label: p.label })),
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
key: 'languages',
|
|
384
|
+
label: 'Language',
|
|
385
|
+
description: 'Languages the AI can speak on calls.',
|
|
386
|
+
type: 'text',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: 'tone',
|
|
390
|
+
label: 'Tone',
|
|
391
|
+
description: 'Conversational style for the AI.',
|
|
392
|
+
type: 'select',
|
|
393
|
+
options: AGENT_TONE_OPTIONS,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
key: 'welcomeMessage',
|
|
397
|
+
label: 'Inbound Greetings',
|
|
398
|
+
description: 'Spoken when a caller connects.',
|
|
399
|
+
type: 'text',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
phone: [
|
|
403
|
+
{
|
|
404
|
+
key: 'answerMode',
|
|
405
|
+
label: 'AI Answer Mode',
|
|
406
|
+
description: 'When the AI picks up inbound calls.',
|
|
407
|
+
type: 'select',
|
|
408
|
+
options: AI_ANSWER_MODE_OPTIONS,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
key: 'mainNumber',
|
|
412
|
+
label: 'AI Phone Number',
|
|
413
|
+
description: 'Number patients dial for the AI receptionist.',
|
|
414
|
+
type: 'text',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
key: 'transferNumber',
|
|
418
|
+
label: 'Transfer Number',
|
|
419
|
+
description: 'Staff / front-desk number for handoff.',
|
|
420
|
+
type: 'text',
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
key: 'ringTimeout',
|
|
424
|
+
label: 'Ringing Time Wait',
|
|
425
|
+
description: 'How long to ring before the AI answers.',
|
|
426
|
+
type: 'select',
|
|
427
|
+
options: RING_TIMEOUT_OPTIONS,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
key: 'createFollowUpTask',
|
|
431
|
+
label: 'Create Follow-up Task',
|
|
432
|
+
description: 'If no one answers in time, the agent auto-creates a high priority task.',
|
|
433
|
+
type: 'boolean',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
key: 'locationIds',
|
|
437
|
+
label: 'Location Handled',
|
|
438
|
+
description: 'Locations this AI receptionist covers.',
|
|
439
|
+
type: 'text',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
key: 'maxCallDurationMin',
|
|
443
|
+
label: 'Max AI Call Duration',
|
|
444
|
+
description: 'Longest call the AI will stay on before wrapping up.',
|
|
445
|
+
type: 'select',
|
|
446
|
+
options: MAX_CALL_DURATION_OPTIONS,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
key: 'silenceTimeoutSec',
|
|
450
|
+
label: 'Silence Timeout',
|
|
451
|
+
description: 'How long silence lasts before the AI prompts or ends.',
|
|
452
|
+
type: 'select',
|
|
453
|
+
options: SILENCE_TIMEOUT_OPTIONS,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
key: 'afterHoursBehavior',
|
|
457
|
+
label: 'After-Hours Behaviour',
|
|
458
|
+
description: 'What happens outside business hours.',
|
|
459
|
+
type: 'select',
|
|
460
|
+
options: AFTER_HOURS_OPTIONS,
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
tasks: [
|
|
464
|
+
{
|
|
465
|
+
key: 'autoCreate',
|
|
466
|
+
label: 'Auto-create tasks',
|
|
467
|
+
description: 'Create a staff task when the AI cannot fully resolve a request.',
|
|
468
|
+
type: 'boolean',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
key: 'defaultAssignee',
|
|
472
|
+
label: 'Default assignee',
|
|
473
|
+
description: 'Team or role new tasks are routed to.',
|
|
474
|
+
type: 'text',
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
scheduling: [
|
|
478
|
+
{
|
|
479
|
+
key: 'leadTimeHours',
|
|
480
|
+
label: 'Minimum lead time (hours)',
|
|
481
|
+
description: 'Earliest bookable slot from now.',
|
|
482
|
+
type: 'number',
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
key: 'allowSameDay',
|
|
486
|
+
label: 'Allow same-day booking',
|
|
487
|
+
description: 'Let patients book appointments later today.',
|
|
488
|
+
type: 'boolean',
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
refill: [
|
|
492
|
+
{
|
|
493
|
+
key: 'autoApproveDays',
|
|
494
|
+
label: 'Auto-approve within (days)',
|
|
495
|
+
description: 'Refills due within this window may be auto-approved.',
|
|
496
|
+
type: 'number',
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
key: 'requireStaffReview',
|
|
500
|
+
label: 'Require staff review',
|
|
501
|
+
description: 'All refill requests need staff approval before sending to pharmacy.',
|
|
502
|
+
type: 'boolean',
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
verify: [
|
|
506
|
+
{
|
|
507
|
+
key: 'requireDob',
|
|
508
|
+
label: 'Require date of birth',
|
|
509
|
+
description: 'Ask for DOB before lookup or scheduling.',
|
|
510
|
+
type: 'boolean',
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
key: 'requireName',
|
|
514
|
+
label: 'Require full name',
|
|
515
|
+
description: 'Ask for patient name during verification.',
|
|
516
|
+
type: 'boolean',
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
key: 'maxAttempts',
|
|
520
|
+
label: 'Max verification attempts',
|
|
521
|
+
description: 'After this many failures, offer transfer to staff.',
|
|
522
|
+
type: 'number',
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
escalation: [
|
|
526
|
+
{
|
|
527
|
+
key: 'sentimentThreshold',
|
|
528
|
+
label: 'Sentiment threshold',
|
|
529
|
+
description: 'Escalate when caller sentiment reaches this level (e.g. frustrated).',
|
|
530
|
+
type: 'text',
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
key: 'urgentKeywords',
|
|
534
|
+
label: 'Urgent keywords',
|
|
535
|
+
description: 'Comma-separated phrases that trigger immediate escalation.',
|
|
536
|
+
type: 'text',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
outbound: [
|
|
540
|
+
{
|
|
541
|
+
key: 'enabled',
|
|
542
|
+
label: 'Outbound calls enabled',
|
|
543
|
+
description: 'Allow staff to place outbound calls from the dashboard.',
|
|
544
|
+
type: 'boolean',
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
key: 'callerId',
|
|
548
|
+
label: 'Outbound caller ID',
|
|
549
|
+
description: 'Number shown to patients on outbound calls.',
|
|
550
|
+
type: 'text',
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
knowledge: [
|
|
554
|
+
{
|
|
555
|
+
key: 'whatToSay',
|
|
556
|
+
label: 'What to say',
|
|
557
|
+
description: 'Facts and guidance the AI may share with callers.',
|
|
558
|
+
type: 'text',
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
key: 'whatNotToSay',
|
|
562
|
+
label: 'What not to say',
|
|
563
|
+
description: 'Topics the AI must refuse or escalate.',
|
|
564
|
+
type: 'text',
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
transcripts: [
|
|
568
|
+
{
|
|
569
|
+
key: 'recordingRetainDays',
|
|
570
|
+
label: 'Keep Call Recordings',
|
|
571
|
+
description: 'How long call recordings are retained.',
|
|
572
|
+
type: 'select',
|
|
573
|
+
options: RETENTION_DAY_OPTIONS,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
key: 'transcriptRetainDays',
|
|
577
|
+
label: 'Keep Transcripts',
|
|
578
|
+
description: 'How long call transcripts are retained.',
|
|
579
|
+
type: 'select',
|
|
580
|
+
options: RETENTION_DAY_OPTIONS,
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
key: 'disclosureWarningEnabled',
|
|
584
|
+
label: 'Disclosure Warning',
|
|
585
|
+
description: 'Play or show a disclosure when the AI answers.',
|
|
586
|
+
type: 'boolean',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
key: 'disclosureWarningText',
|
|
590
|
+
label: 'Disclosure text',
|
|
591
|
+
description: 'Spoken or shown disclosure message.',
|
|
592
|
+
type: 'text',
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
advanced: [
|
|
596
|
+
{
|
|
597
|
+
key: 'modelVersion',
|
|
598
|
+
label: 'Model version',
|
|
599
|
+
description: 'Voice AI model profile (set by Edvak support).',
|
|
600
|
+
type: 'text',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: 'debugMode',
|
|
604
|
+
label: 'Debug mode',
|
|
605
|
+
description: 'Verbose logging for troubleshooting — not for production.',
|
|
606
|
+
type: 'boolean',
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
};
|
|
610
|
+
/** Fallback label for API fields not listed in config. */
|
|
611
|
+
function humanizeFieldKey(key) {
|
|
612
|
+
return key
|
|
613
|
+
.replace(/([A-Z])/g, ' $1')
|
|
614
|
+
.replace(/_/g, ' ')
|
|
615
|
+
.trim()
|
|
616
|
+
.replace(/^\w/, (c) => c.toUpperCase());
|
|
617
|
+
}
|
|
618
|
+
function resolveFieldMeta(section, key, value) {
|
|
619
|
+
const known = SETTINGS_FIELDS[section]?.find((f) => f.key === key);
|
|
620
|
+
if (known)
|
|
621
|
+
return known;
|
|
622
|
+
return {
|
|
623
|
+
key,
|
|
624
|
+
label: humanizeFieldKey(key),
|
|
625
|
+
description: '',
|
|
626
|
+
type: typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'number' : 'text',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function parseCsvList(value) {
|
|
630
|
+
return String(value ?? '')
|
|
631
|
+
.split(',')
|
|
632
|
+
.map((s) => s.trim())
|
|
633
|
+
.filter(Boolean);
|
|
634
|
+
}
|
|
635
|
+
function toCsvList(values) {
|
|
636
|
+
return values.map((v) => v.trim()).filter(Boolean).join(',');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const CUSTOM_PANEL_TABS = new Set([
|
|
640
|
+
'general',
|
|
641
|
+
'phone',
|
|
642
|
+
'transcripts',
|
|
643
|
+
'knowledge',
|
|
644
|
+
'advanced',
|
|
645
|
+
]);
|
|
646
|
+
const HIDDEN_GENERIC_KEYS = new Set([
|
|
647
|
+
'welcomeMessage',
|
|
648
|
+
'practiceName',
|
|
649
|
+
'language',
|
|
650
|
+
'languages',
|
|
651
|
+
'tone',
|
|
652
|
+
'timezone',
|
|
653
|
+
]);
|
|
654
|
+
const KNOWLEDGE_MAX = 8000;
|
|
655
|
+
const DISCLOSURE_MAX = 1000;
|
|
656
|
+
class SettingsPageComponent {
|
|
657
|
+
constructor() {
|
|
658
|
+
this.api = inject(AiReceptionistApiService);
|
|
659
|
+
this.toast = inject(ToastService);
|
|
660
|
+
this.route = inject(ActivatedRoute);
|
|
661
|
+
this.agentOnline = inject(AgentOnlineService);
|
|
662
|
+
this.originalSettings = null;
|
|
663
|
+
this.isFieldReadOnly = isFieldReadOnly;
|
|
664
|
+
this.knowledgeMax = KNOWLEDGE_MAX;
|
|
665
|
+
this.disclosureMax = DISCLOSURE_MAX;
|
|
666
|
+
this.welcomeMax = WELCOME_MESSAGE_MAX;
|
|
667
|
+
this.voicePresets = TTS_VOICE_PRESETS;
|
|
668
|
+
this.toneOptions = AGENT_TONE_OPTIONS;
|
|
669
|
+
this.greetingVariables = GREETING_VARIABLES;
|
|
670
|
+
this.retentionOptions = RETENTION_DAY_OPTIONS;
|
|
671
|
+
this.ringTimeoutOptions = RING_TIMEOUT_OPTIONS;
|
|
672
|
+
this.maxCallDurationOptions = MAX_CALL_DURATION_OPTIONS;
|
|
673
|
+
this.silenceTimeoutOptions = SILENCE_TIMEOUT_OPTIONS;
|
|
674
|
+
this.answerModeOptions = AI_ANSWER_MODE_OPTIONS;
|
|
675
|
+
this.afterHoursOptions = AFTER_HOURS_OPTIONS;
|
|
676
|
+
this.settings = signal(null);
|
|
677
|
+
this.tab = signal('general');
|
|
678
|
+
this.loading = signal(true);
|
|
679
|
+
this.saving = signal(false);
|
|
680
|
+
this.saved = signal(false);
|
|
681
|
+
this.knowledgeTestOpen = signal(false);
|
|
682
|
+
this.locations = signal([]);
|
|
683
|
+
this.confirmOffOpen = signal(false);
|
|
684
|
+
this.online = this.agentOnline.online;
|
|
685
|
+
this.agentLoading = this.agentOnline.loading;
|
|
686
|
+
this.toggling = this.agentOnline.toggling;
|
|
687
|
+
this.tabs = [
|
|
688
|
+
{ id: 'general', label: 'Identity & Greetings' },
|
|
689
|
+
{ id: 'phone', label: 'Phone & Availability' },
|
|
690
|
+
{ id: 'scheduling', label: 'Scheduling Rules' },
|
|
691
|
+
{ id: 'outbound', label: 'Outbound Purpose' },
|
|
692
|
+
{ id: 'knowledge', label: 'Knowledge Base' },
|
|
693
|
+
{ id: 'transcripts', label: 'Data & Compliance' },
|
|
694
|
+
{ id: 'advanced', label: 'Advanced' },
|
|
695
|
+
];
|
|
696
|
+
this.sectionMeta = computed(() => {
|
|
697
|
+
const id = this.tab();
|
|
698
|
+
return SETTINGS_SECTION_META[id] ?? { title: String(id), description: '' };
|
|
699
|
+
});
|
|
700
|
+
this.fieldsForTab = computed(() => {
|
|
701
|
+
const s = this.settings();
|
|
702
|
+
const id = this.tab();
|
|
703
|
+
if (!s || CUSTOM_PANEL_TABS.has(id))
|
|
704
|
+
return [];
|
|
705
|
+
const section = s[id];
|
|
706
|
+
const configured = SETTINGS_FIELDS[id] ?? [];
|
|
707
|
+
const configuredKeys = new Set(configured.map((f) => f.key));
|
|
708
|
+
const extras = Object.keys(section)
|
|
709
|
+
.filter((key) => !HIDDEN_GENERIC_KEYS.has(key) && !configuredKeys.has(key))
|
|
710
|
+
.map((key) => resolveFieldMeta(id, key, section[key]));
|
|
711
|
+
return [...configured, ...extras];
|
|
712
|
+
});
|
|
713
|
+
this.welcomeMessage = computed(() => String(this.settings()?.general['welcomeMessage'] ?? ''));
|
|
714
|
+
this.welcomePlaceholder = computed(() => {
|
|
715
|
+
const s = this.settings();
|
|
716
|
+
if (!s)
|
|
717
|
+
return '';
|
|
718
|
+
return buildAutoWelcomeTemplate(String(s.general['practiceName'] ?? ''), String(s.general['agentName'] ?? ''));
|
|
719
|
+
});
|
|
720
|
+
this.welcomePreview = computed(() => {
|
|
721
|
+
const s = this.settings();
|
|
722
|
+
if (!s)
|
|
723
|
+
return '';
|
|
724
|
+
const raw = resolveWelcomePreview(String(s.general['welcomeMessage'] ?? ''), String(s.general['practiceName'] ?? ''), String(s.general['agentName'] ?? ''));
|
|
725
|
+
return raw
|
|
726
|
+
.replaceAll('{{practiceName}}', String(s.general['practiceName'] || 'our practice'))
|
|
727
|
+
.replaceAll('{{agentName}}', String(s.general['agentName'] || 'your AI receptionist'))
|
|
728
|
+
.replaceAll('{{locationName}}', 'Main location')
|
|
729
|
+
.replaceAll('{{businessHours}}', 'Mon–Fri 8am–5pm');
|
|
730
|
+
});
|
|
731
|
+
this.whatToSay = computed(() => String(this.settings()?.knowledge?.['whatToSay'] ?? ''));
|
|
732
|
+
this.whatNotToSay = computed(() => String(this.settings()?.knowledge?.['whatNotToSay'] ?? ''));
|
|
733
|
+
this.selectedLocationIds = computed(() => parseCsvList(this.settings()?.phone?.['locationIds']));
|
|
734
|
+
this.selectedTone = computed(() => String(this.settings()?.general?.['tone'] ?? 'professional'));
|
|
735
|
+
this.disclosureEnabled = computed(() => this.settings()?.transcripts?.['disclosureWarningEnabled'] !== false);
|
|
736
|
+
this.disclosureText = computed(() => String(this.settings()?.transcripts?.['disclosureWarningText'] ?? DEFAULT_DISCLOSURE_WARNING));
|
|
737
|
+
this.agentSwitchDisabled = computed(() => this.agentLoading() || this.toggling() || this.online() === null);
|
|
738
|
+
}
|
|
739
|
+
ngOnInit() {
|
|
740
|
+
this.agentOnline.refresh();
|
|
741
|
+
const dataTab = this.route.snapshot.data['settingsTab'];
|
|
742
|
+
const tabParam = this.route.snapshot.queryParamMap.get('tab');
|
|
743
|
+
if (dataTab && this.tabs.some((t) => t.id === dataTab)) {
|
|
744
|
+
this.tab.set(dataTab);
|
|
745
|
+
}
|
|
746
|
+
else if (tabParam && this.tabs.some((t) => t.id === tabParam)) {
|
|
747
|
+
this.tab.set(tabParam);
|
|
748
|
+
}
|
|
749
|
+
this.api.getSettings().subscribe({
|
|
750
|
+
next: ({ data }) => {
|
|
751
|
+
this.originalSettings = structuredClone(data);
|
|
752
|
+
this.settings.set(structuredClone(data));
|
|
753
|
+
this.loading.set(false);
|
|
754
|
+
},
|
|
755
|
+
error: () => {
|
|
756
|
+
this.loading.set(false);
|
|
757
|
+
this.toast.show('Failed to load agent settings', 'error');
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
this.api.getLocations().subscribe({
|
|
761
|
+
next: ({ data }) => this.locations.set(data ?? []),
|
|
762
|
+
error: () => this.locations.set([]),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
fieldIsReadOnly(section, key) {
|
|
766
|
+
if (section === 'safety' || section === 'effectiveWelcomeMessage')
|
|
767
|
+
return true;
|
|
768
|
+
return isFieldReadOnly(section, key);
|
|
769
|
+
}
|
|
770
|
+
sectionIsReadOnly(section) {
|
|
771
|
+
if (section === 'safety' || section === 'effectiveWelcomeMessage')
|
|
772
|
+
return true;
|
|
773
|
+
return READ_ONLY_SECTIONS.has(section);
|
|
774
|
+
}
|
|
775
|
+
isTabDisabled(id) {
|
|
776
|
+
return id !== 'safety' && id !== 'effectiveWelcomeMessage' && DISABLED_SETTINGS_TABS.has(id);
|
|
777
|
+
}
|
|
778
|
+
setTab(id) {
|
|
779
|
+
if (this.isTabDisabled(id))
|
|
780
|
+
return;
|
|
781
|
+
this.tab.set(id);
|
|
782
|
+
}
|
|
783
|
+
updateField(section, key, value) {
|
|
784
|
+
const current = this.settings();
|
|
785
|
+
if (!current || section === 'safety' || isFieldReadOnly(section, key))
|
|
786
|
+
return;
|
|
787
|
+
const sectionData = { ...current[section] };
|
|
788
|
+
sectionData[key] = value;
|
|
789
|
+
this.settings.set({ ...current, [section]: sectionData });
|
|
790
|
+
}
|
|
791
|
+
updateWelcomeMessage(value) {
|
|
792
|
+
this.updateField('general', 'welcomeMessage', value.slice(0, WELCOME_MESSAGE_MAX));
|
|
793
|
+
}
|
|
794
|
+
insertGreetingVariable(token) {
|
|
795
|
+
const current = this.welcomeMessage();
|
|
796
|
+
const next = `${current}${current && !/\s$/.test(current) ? ' ' : ''}${token}`;
|
|
797
|
+
this.updateWelcomeMessage(next);
|
|
798
|
+
}
|
|
799
|
+
updateKnowledgeField(key, value) {
|
|
800
|
+
this.updateField('knowledge', key, value.slice(0, KNOWLEDGE_MAX));
|
|
801
|
+
}
|
|
802
|
+
setTone(tone) {
|
|
803
|
+
this.updateField('general', 'tone', tone);
|
|
804
|
+
}
|
|
805
|
+
toggleLocation(id) {
|
|
806
|
+
const current = this.selectedLocationIds();
|
|
807
|
+
const next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
808
|
+
this.updateField('phone', 'locationIds', toCsvList(next));
|
|
809
|
+
}
|
|
810
|
+
selectNumberField(section, key, value) {
|
|
811
|
+
this.updateField(section, key, Number(value));
|
|
812
|
+
}
|
|
813
|
+
numberFieldAsString(section, key) {
|
|
814
|
+
return String(this.fieldValue(section, key) ?? '');
|
|
815
|
+
}
|
|
816
|
+
setDisclosureEnabled(enabled) {
|
|
817
|
+
this.updateField('transcripts', 'disclosureWarningEnabled', enabled);
|
|
818
|
+
if (enabled && !String(this.settings()?.transcripts?.['disclosureWarningText'] ?? '').trim()) {
|
|
819
|
+
this.updateField('transcripts', 'disclosureWarningText', DEFAULT_DISCLOSURE_WARNING);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
updateDisclosureText(value) {
|
|
823
|
+
this.updateField('transcripts', 'disclosureWarningText', value.slice(0, DISCLOSURE_MAX));
|
|
824
|
+
}
|
|
825
|
+
openTestCall() {
|
|
826
|
+
const whatToSay = this.whatToSay().trim();
|
|
827
|
+
const whatNotToSay = this.whatNotToSay().trim();
|
|
828
|
+
if (!whatToSay || !whatNotToSay) {
|
|
829
|
+
this.toast.show('Fill in both “What to say” and “What not to say” before testing.', 'error');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
this.knowledgeTestOpen.set(true);
|
|
833
|
+
}
|
|
834
|
+
closeTestCall() {
|
|
835
|
+
this.knowledgeTestOpen.set(false);
|
|
836
|
+
}
|
|
837
|
+
selectedVoiceId() {
|
|
838
|
+
return String(this.settings()?.general?.['ttsVoiceId'] ?? '').trim();
|
|
839
|
+
}
|
|
840
|
+
onAgentToggleClick() {
|
|
841
|
+
if (this.agentSwitchDisabled())
|
|
842
|
+
return;
|
|
843
|
+
if (this.online()) {
|
|
844
|
+
this.confirmOffOpen.set(true);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
this.applyOnline(true);
|
|
848
|
+
}
|
|
849
|
+
cancelConfirmOff() {
|
|
850
|
+
this.confirmOffOpen.set(false);
|
|
851
|
+
}
|
|
852
|
+
confirmTurnOff() {
|
|
853
|
+
this.confirmOffOpen.set(false);
|
|
854
|
+
this.applyOnline(false);
|
|
855
|
+
}
|
|
856
|
+
applyOnline(enabled) {
|
|
857
|
+
this.agentOnline.setOnline(enabled).subscribe({
|
|
858
|
+
next: () => {
|
|
859
|
+
this.toast.show(enabled
|
|
860
|
+
? 'AI receptionist is online — it will answer inbound calls.'
|
|
861
|
+
: 'AI receptionist is offline — calls go to the front desk.', 'success');
|
|
862
|
+
},
|
|
863
|
+
error: (err) => {
|
|
864
|
+
this.toast.show(err.message ?? 'Failed to update agent status.', 'error');
|
|
865
|
+
this.agentOnline.refresh();
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
fieldValue(section, key) {
|
|
870
|
+
const s = this.settings();
|
|
871
|
+
if (!s || section === 'safety')
|
|
872
|
+
return '';
|
|
873
|
+
return s[section][key];
|
|
874
|
+
}
|
|
875
|
+
save() {
|
|
876
|
+
const s = this.settings();
|
|
877
|
+
const orig = this.originalSettings;
|
|
878
|
+
if (!s || !orig)
|
|
879
|
+
return;
|
|
880
|
+
const payload = structuredClone(s);
|
|
881
|
+
payload.general['welcomeMessage'] = String(payload.general['welcomeMessage'] ?? '').trim();
|
|
882
|
+
payload.general['agentName'] = String(payload.general['agentName'] ?? '').trim();
|
|
883
|
+
if (!String(payload.general['agentName'] ?? '').trim()) {
|
|
884
|
+
this.tab.set('general');
|
|
885
|
+
this.toast.show('Agent name is required.', 'error');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (!String(payload.general['welcomeMessage'] ?? '').trim()) {
|
|
889
|
+
this.tab.set('general');
|
|
890
|
+
this.toast.show('Inbound greeting is required.', 'error');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const whatToSay = String(payload.knowledge?.['whatToSay'] ?? '').trim();
|
|
894
|
+
const whatNotToSay = String(payload.knowledge?.['whatNotToSay'] ?? '').trim();
|
|
895
|
+
if (!whatToSay || !whatNotToSay) {
|
|
896
|
+
this.tab.set('knowledge');
|
|
897
|
+
this.toast.show('Knowledge Base requires both “What to say” and “What not to say”.', 'error');
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
payload.knowledge = { whatToSay, whatNotToSay };
|
|
901
|
+
// Keep legacy retainDays in sync for older BE consumers.
|
|
902
|
+
const recDays = Number(payload.transcripts['recordingRetainDays'] ?? 30);
|
|
903
|
+
const txDays = Number(payload.transcripts['transcriptRetainDays'] ?? 30);
|
|
904
|
+
payload.transcripts['retainDays'] = Math.max(recDays, txDays);
|
|
905
|
+
this.restoreReadOnlyFields(payload, orig);
|
|
906
|
+
this.saving.set(true);
|
|
907
|
+
this.api.putSettings(payload).subscribe({
|
|
908
|
+
next: ({ data }) => {
|
|
909
|
+
this.originalSettings = structuredClone(data);
|
|
910
|
+
this.settings.set(structuredClone(data));
|
|
911
|
+
this.saving.set(false);
|
|
912
|
+
this.saved.set(true);
|
|
913
|
+
this.toast.show('Agent settings saved', 'success');
|
|
914
|
+
setTimeout(() => this.saved.set(false), 2000);
|
|
915
|
+
},
|
|
916
|
+
error: (err) => {
|
|
917
|
+
this.saving.set(false);
|
|
918
|
+
const message = err instanceof Error && err.message.trim()
|
|
919
|
+
? err.message
|
|
920
|
+
: 'Failed to save agent settings';
|
|
921
|
+
this.toast.show(message, 'error');
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
restoreReadOnlyFields(payload, orig) {
|
|
926
|
+
const editableSections = [
|
|
927
|
+
'general',
|
|
928
|
+
'phone',
|
|
929
|
+
'tasks',
|
|
930
|
+
'scheduling',
|
|
931
|
+
'refill',
|
|
932
|
+
'verify',
|
|
933
|
+
'escalation',
|
|
934
|
+
'outbound',
|
|
935
|
+
'knowledge',
|
|
936
|
+
'transcripts',
|
|
937
|
+
'advanced',
|
|
938
|
+
];
|
|
939
|
+
for (const sectionKey of editableSections) {
|
|
940
|
+
if (READ_ONLY_SECTIONS.has(sectionKey)) {
|
|
941
|
+
payload[sectionKey] = structuredClone(orig[sectionKey]);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const payloadSection = payload[sectionKey];
|
|
945
|
+
const origSection = orig[sectionKey];
|
|
946
|
+
for (const key of Object.keys(payloadSection)) {
|
|
947
|
+
if (isFieldReadOnly(sectionKey, key)) {
|
|
948
|
+
payloadSection[key] = origSection[key];
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
954
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: SettingsPageComponent, isStandalone: true, selector: "eds-settings-page", ngImport: i0, template: "<div class=\"eds-page settings-page\">\n <header class=\"settings-page__header\">\n <div>\n <h1 class=\"eds-h1\">Agent settings</h1>\n <p class=\"settings-page__subtitle\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"settings-page__save-row\">\n @if (saved()) {\n <span class=\"settings-page__saved\">Saved</span>\n }\n <eds-button variant=\"primary\" [disabled]=\"saving()\" (click)=\"save()\">\n <eds-icon name=\"save\" [size]=\"18\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </eds-button>\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"settings-page__loading\">Loading settings\u2026</p>\n } @else if (settings()) {\n <div class=\"settings-page__layout\">\n <nav class=\"eds-card settings-page__sidebar\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item\"\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=\"settings-page__nav-disabled-badge\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"eds-card settings-page__panel\">\n <header class=\"settings-page__panel-head\">\n <div class=\"settings-page__panel-head-text\">\n <h2 class=\"settings-page__panel-title\">{{ sectionMeta().title }}</h2>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"settings-page__panel-desc\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"openTestCall()\">\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Test Call\n </eds-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"settings-page__readonly-banner\">\n <eds-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n @if (tab() === 'knowledge') {\n <div class=\"settings-page__knowledge\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n What to say <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea settings-page__textarea--knowledge\"\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=\"settings-page__char-count\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n What not to say <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea settings-page__textarea--knowledge\"\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=\"settings-page__char-count\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </label>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Name <span class=\"settings-page__req\">*</span>\n <eds-icon name=\"info\" [size]=\"16\" class=\"settings-page__info\" title=\"How the AI introduces itself on calls\" />\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <div class=\"settings-page__voice-row\">\n <label class=\"settings-page__field settings-page__field--grow\">\n <span class=\"settings-page__field-label\">\n Voice <span class=\"settings-page__req\">*</span>\n </span>\n <div class=\"settings-page__select-wrap\">\n <select\n class=\"settings-page__input settings-page__select\"\n [ngModel]=\"fieldValue('general', 'ttsVoiceId')\"\n (ngModelChange)=\"updateField('general', 'ttsVoiceId', $event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.label }}</option>\n }\n </select>\n <eds-icon name=\"expand_more\" [size]=\"20\" class=\"settings-page__select-caret\" />\n </div>\n </label>\n </div>\n\n <div class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">Tone</span>\n <div class=\"settings-page__tone-row\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Inbound Greetings <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea\"\n rows=\"4\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"settings-page__char-count\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"settings-page__variables\">\n <span class=\"settings-page__variables-label\">Variables</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </label>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n AI Answer Mode <span class=\"settings-page__req\">*</span>\n <eds-icon name=\"info\" [size]=\"16\" class=\"settings-page__info\" title=\"When the AI picks up inbound calls\" />\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label\n class=\"settings-page__field settings-page__field--full\"\n [class.settings-page__field--readonly]=\"isFieldReadOnly('phone', 'mainNumber')\"\n >\n <span class=\"settings-page__field-label\">\n AI Phone Number <span class=\"settings-page__req\">*</span>\n @if (isFieldReadOnly('phone', 'mainNumber')) {\n }\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <label\n class=\"settings-page__field settings-page__field--full\"\n [class.settings-page__field--readonly]=\"isFieldReadOnly('phone', 'transferNumber')\"\n >\n <span class=\"settings-page__field-label\">\n Transfer Number <span class=\"settings-page__req\">*</span>\n @if (isFieldReadOnly('phone', 'transferNumber')) {\n }\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Ringing Time Wait <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <div class=\"settings-page__feature-card\">\n <eds-icon name=\"event_available\" [size]=\"22\" class=\"settings-page__feature-icon\" />\n <div class=\"settings-page__feature-copy\">\n <span class=\"settings-page__feature-title\">Create Follow-up Task</span>\n <span class=\"settings-page__feature-desc\">\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\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\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=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Location Handled <span class=\"settings-page__req\">*</span>\n </span>\n <div class=\"settings-page__check-grid\" role=\"group\" aria-label=\"Locations handled\">\n @for (loc of locations(); track loc.id) {\n <label class=\"settings-page__check-item\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n autocomplete=\"off\"\n />\n <span>{{ loc.name }}</span>\n </label>\n } @empty {\n <p class=\"settings-page__menu-empty\">No locations available.</p>\n }\n </div>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Max AI Call Duration <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Silence Timeout <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n After-Hours Behaviour <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Keep Call Recordings <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Keep Transcripts <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <div class=\"settings-page__disclosure\">\n <div class=\"settings-page__disclosure-head\">\n <span class=\"settings-page__field-label\">Disclosure Warning</span>\n <button\n type=\"button\"\n class=\"settings-page__switch\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"settings-page__textarea\"\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=\"settings-page__form\">\n <div class=\"settings-page__feature-card\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" class=\"settings-page__feature-icon\" />\n <div class=\"settings-page__feature-copy\">\n <span class=\"settings-page__feature-title\">\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=\"settings-page__feature-desc\">\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\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full settings-page__field--readonly\">\n <span class=\"settings-page__field-label\">\n Model version\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input settings-page__input--readonly\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n </label>\n\n <label class=\"settings-page__toggle-row settings-page__toggle-row--readonly\">\n <div class=\"settings-page__toggle-copy\">\n <span class=\"settings-page__field-label\">\n Debug mode\n </span>\n <span class=\"settings-page__field-desc\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <input\n type=\"checkbox\"\n class=\"settings-page__checkbox\"\n [ngModel]=\"fieldValue('advanced', 'debugMode')\"\n disabled\n autocomplete=\"off\"\n />\n </label>\n </div>\n } @else {\n <div class=\"settings-page__fields\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"settings-page__toggle-row\"\n [class.settings-page__toggle-row--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"settings-page__toggle-copy\">\n <span class=\"settings-page__field-label\">\n {{ field.label }}\n </span>\n @if (field.description) {\n <span class=\"settings-page__field-desc\">{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"settings-page__checkbox\"\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 {\n <label\n class=\"settings-page__field\"\n [class.settings-page__field--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <span class=\"settings-page__field-label\">\n {{ field.label }}\n </span>\n @if (field.description) {\n <span class=\"settings-page__field-desc\">{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n type=\"number\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 if (field.type === 'select' && field.options?.length) {\n <select\n class=\"settings-page__input settings-page__select\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 } @else {\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 </label>\n }\n }\n </div>\n }\n </article>\n </div>\n }\n</div>\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=\"settings-page__confirm-body\">\n While offline, the AI will not answer calls. Callers are routed to the human receptionist.\n </p>\n <div class=\"settings-page__confirm-actions\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"cancelConfirmOff()\">Cancel</eds-button>\n <eds-button variant=\"danger\" type=\"button\" [disabled]=\"toggling()\" (click)=\"confirmTurnOff()\">\n Turn off\n </eds-button>\n </div>\n </app-modal>\n}\n", styles: [".settings-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px}.settings-page__subtitle,.settings-page__loading{margin:6px 0 0;font-size:15px;line-height:1.5;color:var(--eds-color-text-muted, #64748b)}.settings-page__save-row{display:flex;align-items:center;gap:12px;flex-shrink:0}.settings-page__saved{color:var(--eds-color-success, #22c55e);font-weight:500;font-size:14px}.settings-page__layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:20px;align-items:start}.settings-page__sidebar{padding:8px;display:flex;flex-direction:column;gap:2px;position:sticky;top:16px;max-height:calc(100vh - 120px);overflow-y:auto}.settings-page__nav-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border:none;background:none;border-radius:var(--eds-radius-md, 8px);font-family:var(--eds-font-inter, Inter, sans-serif);font-size:14px;font-weight:500;text-align:left;cursor:pointer;color:var(--eds-text-secondary, #53575c);width:100%;line-height:1.3}.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);font-weight:500}.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__nav-disabled-badge{margin-left:auto;font-size:11px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:var(--eds-color-text-muted, #94a3b8)}.settings-page__panel{padding:28px 32px;min-height:420px}.settings-page__panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--eds-color-border, #e2e8f0)}.settings-page__panel-head-text{min-width:0}.settings-page__panel-title{margin:0 0 8px;font-size:22px;font-weight:500;color:var(--eds-text-primary, #41464c);letter-spacing:-.01em}.settings-page__panel-desc{margin:0;font-size:15px;line-height:1.55;color:var(--eds-color-text-muted, #64748b);max-width:640px}.settings-page__req{color:#dc2626;font-weight:500}.settings-page__knowledge{display:flex;flex-direction:column;gap:28px;max-width:920px}.settings-page__textarea--knowledge{min-height:220px;font-size:14px;line-height:1.55}.settings-page__quick-link{display:flex;align-items:center;gap:14px;padding:16px 18px;margin-bottom:24px;text-decoration:none;color:inherit;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc);transition:border-color .15s ease,box-shadow .15s ease}.settings-page__quick-link div{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.settings-page__quick-link:hover{border-color:var(--eds-color-primary, #3681f4);box-shadow:var(--eds-shadow-sm)}.settings-page__quick-link-title{font-size:15px;font-weight:500;color:var(--eds-text-primary, #41464c)}.settings-page__quick-link-desc{font-size:14px;line-height:1.45;color:var(--eds-color-text-muted, #64748b)}.settings-page__fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 28px}.settings-page__field{display:flex;flex-direction:column;gap:6px}.settings-page__field-label{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:14px;font-weight:500;color:var(--eds-text-primary, #41464c);line-height:1.35}.settings-page__readonly-badge{display:none}.settings-page__readonly-banner{display:flex;align-items:center;gap:10px;margin:0 0 20px;padding:12px 14px;border-radius:var(--eds-radius-md, 8px);font-size:14px;line-height:1.45;color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f8fafc);border:1px solid var(--eds-color-border, #e2e8f0)}.settings-page__field--readonly .settings-page__field-desc{color:var(--eds-color-text-soft, #94a3b8)}.settings-page__input--readonly,.settings-page__input: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}.settings-page__toggle-row--readonly{cursor:default;opacity:.92}.settings-page__toggle-row--readonly .settings-page__checkbox:disabled{cursor:not-allowed}.settings-page__field-desc{font-size:13px;line-height:1.45;color:var(--eds-color-text-muted, #64748b);margin-bottom:2px}.settings-page__field--full{grid-column:1/-1}.settings-page__textarea{width:100%;padding:10px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:var(--eds-radius-md, 8px);font-size:15px;line-height:1.5;color:var(--eds-text-primary, #41464c);font-family:inherit;resize:vertical;min-height:96px}.settings-page__textarea:focus{outline:2px solid rgba(54,129,244,.35);outline-offset:1px;border-color:var(--eds-color-primary, #3681f4)}.settings-page__char-count{font-size:12px;color:var(--eds-color-text-muted, #64748b);text-align:right}.settings-page__recording-hint{margin:4px 0 0;font-size:13px;line-height:1.45;color:var(--eds-color-warning, #f59e0b)}.settings-page__welcome{grid-column:1/-1;display:flex;flex-direction:column;gap:14px;padding:18px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__welcome-preview{padding:14px 16px;background:#fff}.settings-page__welcome-preview-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}.settings-page__welcome-preview-text{margin:0;font-size:15px;line-height:1.55;color:var(--eds-text-primary, #41464c);font-style:italic}.settings-page__fields--general{margin-top:0}.settings-page__input{width:100%;padding:10px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:var(--eds-radius-md, 8px);font-size:15px;line-height:1.4;color:var(--eds-text-primary, #41464c);background:#fff}.settings-page__input:focus{outline:2px solid rgba(54,129,244,.35);outline-offset:1px;border-color:var(--eds-color-primary, #3681f4)}.settings-page__input::placeholder{color:var(--eds-color-text-soft, #94a3b8)}.settings-page__select-wrap{position:relative;width:100%}.settings-page__select{appearance:none;-webkit-appearance:none;-moz-appearance:none;cursor:pointer;padding-right:40px;background-image:none}.settings-page__select-caret{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--eds-color-text-muted, #64748b);pointer-events:none}.settings-page__voice-preview-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-top:10px}.settings-page__toggle-row{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc);cursor:pointer}.settings-page__toggle-copy{display:flex;flex-direction:column;gap:4px;min-width:0}.settings-page__checkbox{width:18px;height:18px;flex-shrink:0;accent-color:var(--eds-color-primary, #3681f4);cursor:pointer}.settings-page__confirm-body{margin:0 0 16px;font-size:14px;line-height:1.5;color:var(--eds-color-text-muted, #64748b)}.settings-page__confirm-actions{display:flex;justify-content:flex-end;gap:8px}.settings-page__form{display:flex;flex-direction:column;gap:20px;max-width:720px}.settings-page__info{color:var(--eds-color-text-muted, #94a3b8);margin-left:auto}.settings-page__voice-row{display:flex;align-items:flex-end;gap:10px}.settings-page__field--grow{flex:1;min-width:0}.settings-page__icon-btn{width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid var(--eds-color-border-strong, #cbd5e1);background:#fff;color:var(--eds-brand-600, #2162f0);cursor:pointer;flex-shrink:0}.settings-page__icon-btn:hover:not(:disabled){background:#2162f00f}.settings-page__icon-btn:disabled{opacity:.55;cursor:not-allowed}.settings-page__chips-field{position:relative}.settings-page__chips-trigger{width:100%;min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:10px;background:#fff;cursor:pointer;text-align:left}.settings-page__chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0}.settings-page__chips-placeholder{color:var(--eds-color-text-muted, #94a3b8);font-size:14px}.settings-page__chip{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 8px;border-radius:999px;background:#2162f01a;color:var(--eds-brand-600, #2162f0);font:500 12px/1 var(--eds-font-inter, inherit)}.settings-page__chip--more{background:#2162f029}.settings-page__chip-x{border:none;background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 0 0 2px}.settings-page__menu{position:absolute;z-index:5;top:calc(100% + 6px);left:0;right:0;max-height:220px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--eds-color-border, #e2e8f0);background:#fff;box-shadow:0 8px 24px #0f172a14}.settings-page__menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;font-size:14px;cursor:pointer}.settings-page__menu-item:hover{background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__menu-empty{margin:0;padding:10px;font-size:13px;color:var(--eds-color-text-muted, #64748b)}.settings-page__check-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;padding:12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:10px;background:#fff}.settings-page__check-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;font-size:14px;color:var(--eds-text-primary, #41464c);cursor:pointer}.settings-page__check-item:hover{background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__check-item input{width:16px;height:16px;accent-color:var(--eds-brand-600, #2162f0)}.settings-page__tone-row{display:flex;flex-wrap:wrap;gap:8px}.settings-page__tone{height:36px;padding:0 14px;border-radius:999px;border:1px solid transparent;background:var(--eds-color-surface-alt, #f1f5f9);color:var(--eds-color-text-muted, #64748b);font:500 13px/1 var(--eds-font-inter, inherit);cursor:pointer}.settings-page__tone--active{background:#fff;border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__variables{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}.settings-page__variables-label{font-size:13px;color:var(--eds-color-text-muted, #64748b)}.settings-page__var-chip{height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--eds-color-border, #e2e8f0);background:#fff;color:var(--eds-color-text, #41464c);font:500 12px/1 var(--eds-font-inter, inherit);cursor:pointer}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__preview-line{margin:10px 0 0;font-size:13px;line-height:1.45;color:var(--eds-color-text-muted, #64748b)}.settings-page__feature-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:12px;background:#fff}.settings-page__feature-icon{color:var(--eds-brand-600, #2162f0);flex-shrink:0}.settings-page__feature-copy{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}.settings-page__feature-title{font:500 14px/1.3 var(--eds-font-inter, inherit);color:var(--eds-text-primary, #41464c)}.settings-page__feature-desc{font:400 12px/1.4 var(--eds-font-inter, inherit);color:var(--eds-color-text-muted, #64748b)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:#cbd5e1;padding:3px;cursor:pointer;flex-shrink:0;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__disclosure{display:flex;flex-direction:column;gap:12px}.settings-page__disclosure-head{display:flex;align-items:center;justify-content:space-between;gap:12px}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}.settings-page__sidebar{position:static;max-height:none;flex-direction:row;flex-wrap:wrap}.settings-page__nav-item{width:auto;flex:1 1 auto}.settings-page__fields{grid-template-columns:1fr}.settings-page__panel{padding:20px}.settings-page__voice-row{flex-wrap:wrap}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
955
|
+
}
|
|
956
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SettingsPageComponent, decorators: [{
|
|
957
|
+
type: Component,
|
|
958
|
+
args: [{ selector: 'eds-settings-page', imports: [
|
|
959
|
+
FormsModule,
|
|
960
|
+
EdsButtonComponent,
|
|
961
|
+
EdsIconComponent,
|
|
962
|
+
KnowledgeTestModalComponent,
|
|
963
|
+
ModalComponent,
|
|
964
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page settings-page\">\n <header class=\"settings-page__header\">\n <div>\n <h1 class=\"eds-h1\">Agent settings</h1>\n <p class=\"settings-page__subtitle\">Configure AI receptionist behavior and safety</p>\n </div>\n <div class=\"settings-page__save-row\">\n @if (saved()) {\n <span class=\"settings-page__saved\">Saved</span>\n }\n <eds-button variant=\"primary\" [disabled]=\"saving()\" (click)=\"save()\">\n <eds-icon name=\"save\" [size]=\"18\" />\n {{ saving() ? 'Saving\u2026' : 'Save changes' }}\n </eds-button>\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"settings-page__loading\">Loading settings\u2026</p>\n } @else if (settings()) {\n <div class=\"settings-page__layout\">\n <nav class=\"eds-card settings-page__sidebar\" aria-label=\"Settings sections\">\n @for (t of tabs; track t.id) {\n <button\n type=\"button\"\n class=\"settings-page__nav-item\"\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=\"settings-page__nav-disabled-badge\">Disabled</span>\n }\n </button>\n }\n </nav>\n\n <article class=\"eds-card settings-page__panel\">\n <header class=\"settings-page__panel-head\">\n <div class=\"settings-page__panel-head-text\">\n <h2 class=\"settings-page__panel-title\">{{ sectionMeta().title }}</h2>\n @if (tab() !== 'knowledge' && tab() !== 'general' && tab() !== 'phone' && tab() !== 'transcripts') {\n <p class=\"settings-page__panel-desc\">{{ sectionMeta().description }}</p>\n }\n </div>\n @if (tab() === 'knowledge') {\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"openTestCall()\">\n <eds-icon name=\"phone_in_talk\" [size]=\"18\" />\n Test Call\n </eds-button>\n }\n </header>\n\n @if (sectionIsReadOnly(tab())) {\n <p class=\"settings-page__readonly-banner\">\n <eds-icon name=\"lock\" [size]=\"18\" />\n These settings are managed by Edvak. Contact support to request changes.\n </p>\n }\n\n @if (tab() === 'knowledge') {\n <div class=\"settings-page__knowledge\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n What to say <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea settings-page__textarea--knowledge\"\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=\"settings-page__char-count\">{{ whatToSay().length }}/{{ knowledgeMax }}</span>\n </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n What not to say <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea settings-page__textarea--knowledge\"\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=\"settings-page__char-count\">{{ whatNotToSay().length }}/{{ knowledgeMax }}</span>\n </label>\n </div>\n } @else if (tab() === 'general') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Name <span class=\"settings-page__req\">*</span>\n <eds-icon name=\"info\" [size]=\"16\" class=\"settings-page__info\" title=\"How the AI introduces itself on calls\" />\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [ngModel]=\"fieldValue('general', 'agentName')\"\n (ngModelChange)=\"updateField('general', 'agentName', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <div class=\"settings-page__voice-row\">\n <label class=\"settings-page__field settings-page__field--grow\">\n <span class=\"settings-page__field-label\">\n Voice <span class=\"settings-page__req\">*</span>\n </span>\n <div class=\"settings-page__select-wrap\">\n <select\n class=\"settings-page__input settings-page__select\"\n [ngModel]=\"fieldValue('general', 'ttsVoiceId')\"\n (ngModelChange)=\"updateField('general', 'ttsVoiceId', $event)\"\n autocomplete=\"off\"\n >\n @for (v of voicePresets; track v.id) {\n <option [ngValue]=\"v.id\">{{ v.label }}</option>\n }\n </select>\n <eds-icon name=\"expand_more\" [size]=\"20\" class=\"settings-page__select-caret\" />\n </div>\n </label>\n </div>\n\n <div class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">Tone</span>\n <div class=\"settings-page__tone-row\" role=\"radiogroup\" aria-label=\"Tone\">\n @for (tone of toneOptions; track tone.value) {\n <button\n type=\"button\"\n class=\"settings-page__tone\"\n [class.settings-page__tone--active]=\"selectedTone() === tone.value\"\n (click)=\"setTone(tone.value)\"\n >\n {{ tone.label }}\n </button>\n }\n </div>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Inbound Greetings <span class=\"settings-page__req\">*</span>\n </span>\n <textarea\n class=\"settings-page__textarea\"\n rows=\"4\"\n [maxlength]=\"welcomeMax\"\n [placeholder]=\"welcomePlaceholder()\"\n [ngModel]=\"welcomeMessage()\"\n (ngModelChange)=\"updateWelcomeMessage($event)\"\n autocomplete=\"off\"\n ></textarea>\n <span class=\"settings-page__char-count\">{{ welcomeMessage().length }}/{{ welcomeMax }}</span>\n <div class=\"settings-page__variables\">\n <span class=\"settings-page__variables-label\">Variables</span>\n @for (v of greetingVariables; track v.key) {\n <button\n type=\"button\"\n class=\"settings-page__var-chip\"\n (click)=\"insertGreetingVariable(v.token)\"\n [title]=\"'Insert ' + v.token\"\n >\n {{ v.label }}\n </button>\n }\n </div>\n </label>\n </div>\n } @else if (tab() === 'phone') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n AI Answer Mode <span class=\"settings-page__req\">*</span>\n <eds-icon name=\"info\" [size]=\"16\" class=\"settings-page__info\" title=\"When the AI picks up inbound calls\" />\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label\n class=\"settings-page__field settings-page__field--full\"\n [class.settings-page__field--readonly]=\"isFieldReadOnly('phone', 'mainNumber')\"\n >\n <span class=\"settings-page__field-label\">\n AI Phone Number <span class=\"settings-page__req\">*</span>\n @if (isFieldReadOnly('phone', 'mainNumber')) {\n }\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [disabled]=\"isFieldReadOnly('phone', 'mainNumber')\"\n [ngModel]=\"fieldValue('phone', 'mainNumber')\"\n (ngModelChange)=\"updateField('phone', 'mainNumber', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <label\n class=\"settings-page__field settings-page__field--full\"\n [class.settings-page__field--readonly]=\"isFieldReadOnly('phone', 'transferNumber')\"\n >\n <span class=\"settings-page__field-label\">\n Transfer Number <span class=\"settings-page__req\">*</span>\n @if (isFieldReadOnly('phone', 'transferNumber')) {\n }\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [disabled]=\"isFieldReadOnly('phone', 'transferNumber')\"\n [ngModel]=\"fieldValue('phone', 'transferNumber')\"\n (ngModelChange)=\"updateField('phone', 'transferNumber', $event)\"\n autocomplete=\"off\"\n />\n </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Ringing Time Wait <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <div class=\"settings-page__feature-card\">\n <eds-icon name=\"event_available\" [size]=\"22\" class=\"settings-page__feature-icon\" />\n <div class=\"settings-page__feature-copy\">\n <span class=\"settings-page__feature-title\">Create Follow-up Task</span>\n <span class=\"settings-page__feature-desc\">\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\"\n [class.settings-page__switch--on]=\"!!fieldValue('phone', 'createFollowUpTask')\"\n [attr.aria-pressed]=\"!!fieldValue('phone', 'createFollowUpTask')\"\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=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Location Handled <span class=\"settings-page__req\">*</span>\n </span>\n <div class=\"settings-page__check-grid\" role=\"group\" aria-label=\"Locations handled\">\n @for (loc of locations(); track loc.id) {\n <label class=\"settings-page__check-item\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedLocationIds().includes(loc.id)\"\n (change)=\"toggleLocation(loc.id)\"\n autocomplete=\"off\"\n />\n <span>{{ loc.name }}</span>\n </label>\n } @empty {\n <p class=\"settings-page__menu-empty\">No locations available.</p>\n }\n </div>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Max AI Call Duration <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Silence Timeout <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n After-Hours Behaviour <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n </div>\n } @else if (tab() === 'transcripts') {\n <div class=\"settings-page__form\">\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Keep Call Recordings <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <label class=\"settings-page__field settings-page__field--full\">\n <span class=\"settings-page__field-label\">\n Keep Transcripts <span class=\"settings-page__req\">*</span>\n </span>\n <select\n class=\"settings-page__input settings-page__select\"\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 </label>\n\n <div class=\"settings-page__disclosure\">\n <div class=\"settings-page__disclosure-head\">\n <span class=\"settings-page__field-label\">Disclosure Warning</span>\n <button\n type=\"button\"\n class=\"settings-page__switch\"\n [class.settings-page__switch--on]=\"disclosureEnabled()\"\n [attr.aria-pressed]=\"disclosureEnabled()\"\n (click)=\"setDisclosureEnabled(!disclosureEnabled())\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n @if (disclosureEnabled()) {\n <textarea\n class=\"settings-page__textarea\"\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=\"settings-page__form\">\n <div class=\"settings-page__feature-card\">\n <eds-icon name=\"smart_toy\" [size]=\"22\" class=\"settings-page__feature-icon\" />\n <div class=\"settings-page__feature-copy\">\n <span class=\"settings-page__feature-title\">\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=\"settings-page__feature-desc\">\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\"\n [class.settings-page__switch--on]=\"online() === true\"\n [disabled]=\"agentSwitchDisabled()\"\n [attr.aria-pressed]=\"online() === true\"\n (click)=\"onAgentToggleClick()\"\n >\n <span class=\"settings-page__switch-thumb\"></span>\n </button>\n </div>\n\n <label class=\"settings-page__field settings-page__field--full settings-page__field--readonly\">\n <span class=\"settings-page__field-label\">\n Model version\n </span>\n <input\n type=\"text\"\n class=\"settings-page__input settings-page__input--readonly\"\n [ngModel]=\"fieldValue('advanced', 'modelVersion')\"\n disabled\n autocomplete=\"off\"\n />\n </label>\n\n <label class=\"settings-page__toggle-row settings-page__toggle-row--readonly\">\n <div class=\"settings-page__toggle-copy\">\n <span class=\"settings-page__field-label\">\n Debug mode\n </span>\n <span class=\"settings-page__field-desc\">Verbose logging \u2014 contact Edvak to change.</span>\n </div>\n <input\n type=\"checkbox\"\n class=\"settings-page__checkbox\"\n [ngModel]=\"fieldValue('advanced', 'debugMode')\"\n disabled\n autocomplete=\"off\"\n />\n </label>\n </div>\n } @else {\n <div class=\"settings-page__fields\">\n @for (field of fieldsForTab(); track field.key) {\n @if (field.type === 'boolean') {\n <label\n class=\"settings-page__toggle-row\"\n [class.settings-page__toggle-row--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <div class=\"settings-page__toggle-copy\">\n <span class=\"settings-page__field-label\">\n {{ field.label }}\n </span>\n @if (field.description) {\n <span class=\"settings-page__field-desc\">{{ field.description }}</span>\n }\n </div>\n <input\n type=\"checkbox\"\n class=\"settings-page__checkbox\"\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 {\n <label\n class=\"settings-page__field\"\n [class.settings-page__field--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\n >\n <span class=\"settings-page__field-label\">\n {{ field.label }}\n </span>\n @if (field.description) {\n <span class=\"settings-page__field-desc\">{{ field.description }}</span>\n }\n @if (field.type === 'number') {\n <input\n type=\"number\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 if (field.type === 'select' && field.options?.length) {\n <select\n class=\"settings-page__input settings-page__select\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 } @else {\n <input\n type=\"text\"\n class=\"settings-page__input\"\n [class.settings-page__input--readonly]=\"fieldIsReadOnly(tab(), field.key)\"\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 </label>\n }\n }\n </div>\n }\n </article>\n </div>\n }\n</div>\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=\"settings-page__confirm-body\">\n While offline, the AI will not answer calls. Callers are routed to the human receptionist.\n </p>\n <div class=\"settings-page__confirm-actions\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"cancelConfirmOff()\">Cancel</eds-button>\n <eds-button variant=\"danger\" type=\"button\" [disabled]=\"toggling()\" (click)=\"confirmTurnOff()\">\n Turn off\n </eds-button>\n </div>\n </app-modal>\n}\n", styles: [".settings-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px}.settings-page__subtitle,.settings-page__loading{margin:6px 0 0;font-size:15px;line-height:1.5;color:var(--eds-color-text-muted, #64748b)}.settings-page__save-row{display:flex;align-items:center;gap:12px;flex-shrink:0}.settings-page__saved{color:var(--eds-color-success, #22c55e);font-weight:500;font-size:14px}.settings-page__layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:20px;align-items:start}.settings-page__sidebar{padding:8px;display:flex;flex-direction:column;gap:2px;position:sticky;top:16px;max-height:calc(100vh - 120px);overflow-y:auto}.settings-page__nav-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border:none;background:none;border-radius:var(--eds-radius-md, 8px);font-family:var(--eds-font-inter, Inter, sans-serif);font-size:14px;font-weight:500;text-align:left;cursor:pointer;color:var(--eds-text-secondary, #53575c);width:100%;line-height:1.3}.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);font-weight:500}.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__nav-disabled-badge{margin-left:auto;font-size:11px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:var(--eds-color-text-muted, #94a3b8)}.settings-page__panel{padding:28px 32px;min-height:420px}.settings-page__panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--eds-color-border, #e2e8f0)}.settings-page__panel-head-text{min-width:0}.settings-page__panel-title{margin:0 0 8px;font-size:22px;font-weight:500;color:var(--eds-text-primary, #41464c);letter-spacing:-.01em}.settings-page__panel-desc{margin:0;font-size:15px;line-height:1.55;color:var(--eds-color-text-muted, #64748b);max-width:640px}.settings-page__req{color:#dc2626;font-weight:500}.settings-page__knowledge{display:flex;flex-direction:column;gap:28px;max-width:920px}.settings-page__textarea--knowledge{min-height:220px;font-size:14px;line-height:1.55}.settings-page__quick-link{display:flex;align-items:center;gap:14px;padding:16px 18px;margin-bottom:24px;text-decoration:none;color:inherit;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc);transition:border-color .15s ease,box-shadow .15s ease}.settings-page__quick-link div{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.settings-page__quick-link:hover{border-color:var(--eds-color-primary, #3681f4);box-shadow:var(--eds-shadow-sm)}.settings-page__quick-link-title{font-size:15px;font-weight:500;color:var(--eds-text-primary, #41464c)}.settings-page__quick-link-desc{font-size:14px;line-height:1.45;color:var(--eds-color-text-muted, #64748b)}.settings-page__fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 28px}.settings-page__field{display:flex;flex-direction:column;gap:6px}.settings-page__field-label{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:14px;font-weight:500;color:var(--eds-text-primary, #41464c);line-height:1.35}.settings-page__readonly-badge{display:none}.settings-page__readonly-banner{display:flex;align-items:center;gap:10px;margin:0 0 20px;padding:12px 14px;border-radius:var(--eds-radius-md, 8px);font-size:14px;line-height:1.45;color:var(--eds-color-text-muted, #64748b);background:var(--eds-color-surface-alt, #f8fafc);border:1px solid var(--eds-color-border, #e2e8f0)}.settings-page__field--readonly .settings-page__field-desc{color:var(--eds-color-text-soft, #94a3b8)}.settings-page__input--readonly,.settings-page__input: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}.settings-page__toggle-row--readonly{cursor:default;opacity:.92}.settings-page__toggle-row--readonly .settings-page__checkbox:disabled{cursor:not-allowed}.settings-page__field-desc{font-size:13px;line-height:1.45;color:var(--eds-color-text-muted, #64748b);margin-bottom:2px}.settings-page__field--full{grid-column:1/-1}.settings-page__textarea{width:100%;padding:10px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:var(--eds-radius-md, 8px);font-size:15px;line-height:1.5;color:var(--eds-text-primary, #41464c);font-family:inherit;resize:vertical;min-height:96px}.settings-page__textarea:focus{outline:2px solid rgba(54,129,244,.35);outline-offset:1px;border-color:var(--eds-color-primary, #3681f4)}.settings-page__char-count{font-size:12px;color:var(--eds-color-text-muted, #64748b);text-align:right}.settings-page__recording-hint{margin:4px 0 0;font-size:13px;line-height:1.45;color:var(--eds-color-warning, #f59e0b)}.settings-page__welcome{grid-column:1/-1;display:flex;flex-direction:column;gap:14px;padding:18px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__welcome-preview{padding:14px 16px;background:#fff}.settings-page__welcome-preview-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}.settings-page__welcome-preview-text{margin:0;font-size:15px;line-height:1.55;color:var(--eds-text-primary, #41464c);font-style:italic}.settings-page__fields--general{margin-top:0}.settings-page__input{width:100%;padding:10px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:var(--eds-radius-md, 8px);font-size:15px;line-height:1.4;color:var(--eds-text-primary, #41464c);background:#fff}.settings-page__input:focus{outline:2px solid rgba(54,129,244,.35);outline-offset:1px;border-color:var(--eds-color-primary, #3681f4)}.settings-page__input::placeholder{color:var(--eds-color-text-soft, #94a3b8)}.settings-page__select-wrap{position:relative;width:100%}.settings-page__select{appearance:none;-webkit-appearance:none;-moz-appearance:none;cursor:pointer;padding-right:40px;background-image:none}.settings-page__select-caret{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--eds-color-text-muted, #64748b);pointer-events:none}.settings-page__voice-preview-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-top:10px}.settings-page__toggle-row{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:var(--eds-radius-md, 8px);background:var(--eds-color-surface-alt, #f8fafc);cursor:pointer}.settings-page__toggle-copy{display:flex;flex-direction:column;gap:4px;min-width:0}.settings-page__checkbox{width:18px;height:18px;flex-shrink:0;accent-color:var(--eds-color-primary, #3681f4);cursor:pointer}.settings-page__confirm-body{margin:0 0 16px;font-size:14px;line-height:1.5;color:var(--eds-color-text-muted, #64748b)}.settings-page__confirm-actions{display:flex;justify-content:flex-end;gap:8px}.settings-page__form{display:flex;flex-direction:column;gap:20px;max-width:720px}.settings-page__info{color:var(--eds-color-text-muted, #94a3b8);margin-left:auto}.settings-page__voice-row{display:flex;align-items:flex-end;gap:10px}.settings-page__field--grow{flex:1;min-width:0}.settings-page__icon-btn{width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid var(--eds-color-border-strong, #cbd5e1);background:#fff;color:var(--eds-brand-600, #2162f0);cursor:pointer;flex-shrink:0}.settings-page__icon-btn:hover:not(:disabled){background:#2162f00f}.settings-page__icon-btn:disabled{opacity:.55;cursor:not-allowed}.settings-page__chips-field{position:relative}.settings-page__chips-trigger{width:100%;min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:10px;background:#fff;cursor:pointer;text-align:left}.settings-page__chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0}.settings-page__chips-placeholder{color:var(--eds-color-text-muted, #94a3b8);font-size:14px}.settings-page__chip{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 8px;border-radius:999px;background:#2162f01a;color:var(--eds-brand-600, #2162f0);font:500 12px/1 var(--eds-font-inter, inherit)}.settings-page__chip--more{background:#2162f029}.settings-page__chip-x{border:none;background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 0 0 2px}.settings-page__menu{position:absolute;z-index:5;top:calc(100% + 6px);left:0;right:0;max-height:220px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--eds-color-border, #e2e8f0);background:#fff;box-shadow:0 8px 24px #0f172a14}.settings-page__menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;font-size:14px;cursor:pointer}.settings-page__menu-item:hover{background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__menu-empty{margin:0;padding:10px;font-size:13px;color:var(--eds-color-text-muted, #64748b)}.settings-page__check-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;padding:12px;border:1px solid var(--eds-color-border-strong, #cbd5e1);border-radius:10px;background:#fff}.settings-page__check-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;font-size:14px;color:var(--eds-text-primary, #41464c);cursor:pointer}.settings-page__check-item:hover{background:var(--eds-color-surface-alt, #f8fafc)}.settings-page__check-item input{width:16px;height:16px;accent-color:var(--eds-brand-600, #2162f0)}.settings-page__tone-row{display:flex;flex-wrap:wrap;gap:8px}.settings-page__tone{height:36px;padding:0 14px;border-radius:999px;border:1px solid transparent;background:var(--eds-color-surface-alt, #f1f5f9);color:var(--eds-color-text-muted, #64748b);font:500 13px/1 var(--eds-font-inter, inherit);cursor:pointer}.settings-page__tone--active{background:#fff;border-color:var(--eds-brand-600, #2162f0);color:var(--eds-brand-600, #2162f0)}.settings-page__variables{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}.settings-page__variables-label{font-size:13px;color:var(--eds-color-text-muted, #64748b)}.settings-page__var-chip{height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--eds-color-border, #e2e8f0);background:#fff;color:var(--eds-color-text, #41464c);font:500 12px/1 var(--eds-font-inter, inherit);cursor:pointer}.settings-page__var-chip:hover{border-color:var(--eds-brand-300, #94c5f9);color:var(--eds-brand-600, #2162f0)}.settings-page__preview-line{margin:10px 0 0;font-size:13px;line-height:1.45;color:var(--eds-color-text-muted, #64748b)}.settings-page__feature-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--eds-color-border, #e2e8f0);border-radius:12px;background:#fff}.settings-page__feature-icon{color:var(--eds-brand-600, #2162f0);flex-shrink:0}.settings-page__feature-copy{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}.settings-page__feature-title{font:500 14px/1.3 var(--eds-font-inter, inherit);color:var(--eds-text-primary, #41464c)}.settings-page__feature-desc{font:400 12px/1.4 var(--eds-font-inter, inherit);color:var(--eds-color-text-muted, #64748b)}.settings-page__switch{width:44px;height:26px;border-radius:999px;border:none;background:#cbd5e1;padding:3px;cursor:pointer;flex-shrink:0;transition:background .15s ease}.settings-page__switch--on{background:var(--eds-brand-600, #2162f0)}.settings-page__switch:disabled{opacity:.55;cursor:not-allowed}.settings-page__switch-thumb{display:block;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a33;transition:transform .15s ease}.settings-page__switch--on .settings-page__switch-thumb{transform:translate(18px)}.settings-page__disclosure{display:flex;flex-direction:column;gap:12px}.settings-page__disclosure-head{display:flex;align-items:center;justify-content:space-between;gap:12px}@media(max-width:960px){.settings-page__layout{grid-template-columns:1fr}.settings-page__sidebar{position:static;max-height:none;flex-direction:row;flex-wrap:wrap}.settings-page__nav-item{width:auto;flex:1 1 auto}.settings-page__fields{grid-template-columns:1fr}.settings-page__panel{padding:20px}.settings-page__voice-row{flex-wrap:wrap}}\n"] }]
|
|
965
|
+
}] });
|
|
966
|
+
|
|
967
|
+
export { SettingsPageComponent };
|
|
968
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map
|