@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
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, inject, EventEmitter, Output, ChangeDetectionStrategy, Component, Input, computed } from '@angular/core';
|
|
3
|
-
import { DatePipe } from '@angular/common';
|
|
4
|
-
import * as i1 from '@angular/forms';
|
|
5
|
-
import { FormsModule } from '@angular/forms';
|
|
6
|
-
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
7
|
-
import { of, delay, map } from 'rxjs';
|
|
8
|
-
import { V as VOICE_RECEPTIONIST_CONFIG, e as createVoiceReceptionistPaths, D as DEFAULT_PAGE_SIZE, j as asPaginatedResult, E as EdsButtonComponent, M as ModalComponent, T as ToastService, f as totalPages, b as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-Dn0GrZ6_.mjs';
|
|
9
|
-
|
|
10
|
-
const FAQ_CATEGORY_PRESETS = [
|
|
11
|
-
'hours',
|
|
12
|
-
'location',
|
|
13
|
-
'parking',
|
|
14
|
-
'insurance',
|
|
15
|
-
'services',
|
|
16
|
-
'policies',
|
|
17
|
-
'general',
|
|
18
|
-
];
|
|
19
|
-
const FAQ_STARTER_TEMPLATES = [
|
|
20
|
-
{
|
|
21
|
-
label: 'Office hours',
|
|
22
|
-
question: 'What are your office hours?',
|
|
23
|
-
answer: 'We are open Monday through Friday, 9 AM to 6 PM Eastern.',
|
|
24
|
-
category: 'hours',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
label: 'Parking / directions',
|
|
28
|
-
question: 'Where should I park when I visit?',
|
|
29
|
-
answer: 'Free parking is available in the lot behind the building on Oak Street.',
|
|
30
|
-
category: 'parking',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
label: 'Insurance accepted',
|
|
34
|
-
question: 'Do you accept my insurance?',
|
|
35
|
-
answer: 'We accept most major insurance plans. Please call reception to confirm your specific plan.',
|
|
36
|
-
category: 'insurance',
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
const SEED_PRACTICE_FAQS = [
|
|
42
|
-
{
|
|
43
|
-
faqId: 'faq-1',
|
|
44
|
-
question: 'What are your office hours?',
|
|
45
|
-
answer: 'We are open Monday through Friday, 9 AM to 6 PM Eastern.',
|
|
46
|
-
category: 'hours',
|
|
47
|
-
sortOrder: 0,
|
|
48
|
-
isActive: true,
|
|
49
|
-
hasEmbedding: true,
|
|
50
|
-
createdAt: new Date(now - 86400000 * 14).toISOString(),
|
|
51
|
-
updatedAt: new Date(now - 86400000 * 2).toISOString(),
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
faqId: 'faq-2',
|
|
55
|
-
question: 'Where is the clinic located?',
|
|
56
|
-
answer: 'We are at 1200 Riverside Drive, Suite 200, Austin, Texas.',
|
|
57
|
-
category: 'location',
|
|
58
|
-
sortOrder: 1,
|
|
59
|
-
isActive: true,
|
|
60
|
-
hasEmbedding: true,
|
|
61
|
-
createdAt: new Date(now - 86400000 * 10).toISOString(),
|
|
62
|
-
updatedAt: new Date(now - 86400000 * 5).toISOString(),
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
faqId: 'faq-3',
|
|
66
|
-
question: 'Do you have parking?',
|
|
67
|
-
answer: 'Yes, free patient parking is available in the lot behind the building.',
|
|
68
|
-
category: 'parking',
|
|
69
|
-
sortOrder: 2,
|
|
70
|
-
isActive: true,
|
|
71
|
-
hasEmbedding: true,
|
|
72
|
-
createdAt: new Date(now - 86400000 * 8).toISOString(),
|
|
73
|
-
updatedAt: new Date(now - 86400000 * 8).toISOString(),
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
faqId: 'faq-4',
|
|
77
|
-
question: 'Which insurance plans do you accept?',
|
|
78
|
-
answer: 'We accept most major plans including Blue Cross, Aetna, and UnitedHealthcare.',
|
|
79
|
-
category: 'insurance',
|
|
80
|
-
sortOrder: 3,
|
|
81
|
-
isActive: true,
|
|
82
|
-
hasEmbedding: false,
|
|
83
|
-
createdAt: new Date(now - 86400000 * 6).toISOString(),
|
|
84
|
-
updatedAt: new Date(now - 86400000 * 1).toISOString(),
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
faqId: 'faq-5',
|
|
88
|
-
question: 'How do I request a callback?',
|
|
89
|
-
answer: 'Leave a message with your name and number and we will call you back within one business day.',
|
|
90
|
-
category: 'general',
|
|
91
|
-
sortOrder: 4,
|
|
92
|
-
isActive: false,
|
|
93
|
-
hasEmbedding: true,
|
|
94
|
-
createdAt: new Date(now - 86400000 * 3).toISOString(),
|
|
95
|
-
updatedAt: new Date(now - 86400000 * 3).toISOString(),
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
const MOCK_DELAY_MS = 200;
|
|
100
|
-
class PracticeFaqApiMockService {
|
|
101
|
-
constructor() {
|
|
102
|
-
this.faqs = signal(structuredClone(SEED_PRACTICE_FAQS));
|
|
103
|
-
}
|
|
104
|
-
list(activeOnly = false) {
|
|
105
|
-
let rows = [...this.faqs()].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
106
|
-
if (activeOnly)
|
|
107
|
-
rows = rows.filter((f) => f.isActive);
|
|
108
|
-
return of(structuredClone(rows)).pipe(delay(MOCK_DELAY_MS));
|
|
109
|
-
}
|
|
110
|
-
get(faqId) {
|
|
111
|
-
const row = this.faqs().find((f) => f.faqId === faqId) ?? null;
|
|
112
|
-
return of(row ? structuredClone(row) : null).pipe(delay(MOCK_DELAY_MS));
|
|
113
|
-
}
|
|
114
|
-
create(body) {
|
|
115
|
-
const now = new Date().toISOString();
|
|
116
|
-
const faq = {
|
|
117
|
-
faqId: `faq-${Date.now()}`,
|
|
118
|
-
question: body.question.trim(),
|
|
119
|
-
answer: body.answer.trim(),
|
|
120
|
-
category: body.category?.trim() || null,
|
|
121
|
-
sortOrder: body.sortOrder ?? this.faqs().length,
|
|
122
|
-
isActive: body.isActive ?? true,
|
|
123
|
-
hasEmbedding: true,
|
|
124
|
-
createdAt: now,
|
|
125
|
-
updatedAt: now,
|
|
126
|
-
};
|
|
127
|
-
this.faqs.update((list) => [...list, faq]);
|
|
128
|
-
return of(structuredClone(faq)).pipe(delay(MOCK_DELAY_MS));
|
|
129
|
-
}
|
|
130
|
-
update(faqId, body) {
|
|
131
|
-
let updated;
|
|
132
|
-
this.faqs.update((list) => list.map((f) => {
|
|
133
|
-
if (f.faqId !== faqId)
|
|
134
|
-
return f;
|
|
135
|
-
updated = {
|
|
136
|
-
...f,
|
|
137
|
-
question: body.question?.trim() ?? f.question,
|
|
138
|
-
answer: body.answer?.trim() ?? f.answer,
|
|
139
|
-
category: body.category !== undefined ? body.category : f.category,
|
|
140
|
-
sortOrder: body.sortOrder ?? f.sortOrder,
|
|
141
|
-
isActive: body.isActive ?? f.isActive,
|
|
142
|
-
hasEmbedding: true,
|
|
143
|
-
updatedAt: new Date().toISOString(),
|
|
144
|
-
};
|
|
145
|
-
return updated;
|
|
146
|
-
}));
|
|
147
|
-
if (!updated)
|
|
148
|
-
throw new Error('FAQ not found');
|
|
149
|
-
const saved = updated;
|
|
150
|
-
return of(structuredClone(saved)).pipe(delay(MOCK_DELAY_MS));
|
|
151
|
-
}
|
|
152
|
-
delete(faqId) {
|
|
153
|
-
this.faqs.update((list) => list.filter((f) => f.faqId !== faqId));
|
|
154
|
-
return of({ success: true }).pipe(delay(MOCK_DELAY_MS));
|
|
155
|
-
}
|
|
156
|
-
search(q, limit = 3) {
|
|
157
|
-
const query = q.trim().toLowerCase();
|
|
158
|
-
if (!query) {
|
|
159
|
-
return of({ found: false, items: [] }).pipe(delay(MOCK_DELAY_MS));
|
|
160
|
-
}
|
|
161
|
-
const scored = this.faqs()
|
|
162
|
-
.filter((f) => f.isActive)
|
|
163
|
-
.map((f) => {
|
|
164
|
-
const hay = `${f.question} ${f.answer}`.toLowerCase();
|
|
165
|
-
const hit = hay.includes(query);
|
|
166
|
-
const score = hit ? Math.min(0.95, 0.55 + query.length / Math.max(hay.length, 1)) : 0;
|
|
167
|
-
return { faq: f, score };
|
|
168
|
-
})
|
|
169
|
-
.filter((r) => r.score > 0)
|
|
170
|
-
.sort((a, b) => b.score - a.score)
|
|
171
|
-
.slice(0, limit);
|
|
172
|
-
const items = scored.map(({ faq, score }) => ({
|
|
173
|
-
faqId: faq.faqId,
|
|
174
|
-
question: faq.question,
|
|
175
|
-
answer: faq.answer,
|
|
176
|
-
category: faq.category,
|
|
177
|
-
score: Math.round(score * 100) / 100,
|
|
178
|
-
}));
|
|
179
|
-
return of({ found: items.length > 0, items }).pipe(delay(MOCK_DELAY_MS));
|
|
180
|
-
}
|
|
181
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiMockService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
182
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiMockService, providedIn: 'root' }); }
|
|
183
|
-
}
|
|
184
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiMockService, decorators: [{
|
|
185
|
-
type: Injectable,
|
|
186
|
-
args: [{ providedIn: 'root' }]
|
|
187
|
-
}] });
|
|
188
|
-
|
|
189
|
-
class PracticeFaqApiService {
|
|
190
|
-
constructor() {
|
|
191
|
-
this.http = inject(HttpClient);
|
|
192
|
-
this.mock = inject(PracticeFaqApiMockService);
|
|
193
|
-
this.config = inject(VOICE_RECEPTIONIST_CONFIG);
|
|
194
|
-
this.paths = createVoiceReceptionistPaths(this.config);
|
|
195
|
-
this.base = `${this.paths.aiReceptionistPath()}/knowledge/faqs`;
|
|
196
|
-
}
|
|
197
|
-
get useMock() {
|
|
198
|
-
return !!this.config.useMockApi;
|
|
199
|
-
}
|
|
200
|
-
list(opts) {
|
|
201
|
-
const page = opts?.page ?? 1;
|
|
202
|
-
const limit = opts?.limit ?? DEFAULT_PAGE_SIZE;
|
|
203
|
-
if (this.useMock) {
|
|
204
|
-
return this.mock.list(!!opts?.activeOnly).pipe(map((items) => {
|
|
205
|
-
const start = (page - 1) * limit;
|
|
206
|
-
return {
|
|
207
|
-
items: items.slice(start, start + limit),
|
|
208
|
-
total: items.length,
|
|
209
|
-
page,
|
|
210
|
-
limit,
|
|
211
|
-
};
|
|
212
|
-
}));
|
|
213
|
-
}
|
|
214
|
-
let params = new HttpParams().set('page', String(page)).set('limit', String(limit));
|
|
215
|
-
if (opts?.activeOnly)
|
|
216
|
-
params = params.set('activeOnly', 'true');
|
|
217
|
-
return this.http.get(this.base, { params }).pipe(map((body) => asPaginatedResult(body, page, limit)));
|
|
218
|
-
}
|
|
219
|
-
get(faqId) {
|
|
220
|
-
if (this.useMock)
|
|
221
|
-
return this.mock.get(faqId);
|
|
222
|
-
const enc = encodeURIComponent(faqId);
|
|
223
|
-
return this.http.get(`${this.base}/${enc}`);
|
|
224
|
-
}
|
|
225
|
-
create(body) {
|
|
226
|
-
if (this.useMock)
|
|
227
|
-
return this.mock.create(body);
|
|
228
|
-
return this.http.post(this.base, body);
|
|
229
|
-
}
|
|
230
|
-
update(faqId, body) {
|
|
231
|
-
if (this.useMock)
|
|
232
|
-
return this.mock.update(faqId, body);
|
|
233
|
-
const enc = encodeURIComponent(faqId);
|
|
234
|
-
return this.http.patch(`${this.base}/${enc}`, body);
|
|
235
|
-
}
|
|
236
|
-
delete(faqId) {
|
|
237
|
-
if (this.useMock)
|
|
238
|
-
return this.mock.delete(faqId);
|
|
239
|
-
const enc = encodeURIComponent(faqId);
|
|
240
|
-
return this.http.delete(`${this.base}/${enc}`);
|
|
241
|
-
}
|
|
242
|
-
search(q, limit = 3) {
|
|
243
|
-
if (this.useMock)
|
|
244
|
-
return this.mock.search(q, limit);
|
|
245
|
-
const params = new HttpParams().set('q', q).set('limit', String(limit));
|
|
246
|
-
return this.http.get(`${this.base}/search`, { params });
|
|
247
|
-
}
|
|
248
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
249
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, providedIn: 'root' }); }
|
|
250
|
-
}
|
|
251
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, decorators: [{
|
|
252
|
-
type: Injectable,
|
|
253
|
-
args: [{ providedIn: 'root' }]
|
|
254
|
-
}] });
|
|
255
|
-
|
|
256
|
-
function categoryBadgeClass(category) {
|
|
257
|
-
if (!category)
|
|
258
|
-
return 'faq-chip--general';
|
|
259
|
-
const safe = category.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
260
|
-
return `faq-chip--${safe || 'general'}`;
|
|
261
|
-
}
|
|
262
|
-
function truncateText(text, max = 80) {
|
|
263
|
-
const t = text?.trim() ?? '';
|
|
264
|
-
if (t.length <= max)
|
|
265
|
-
return t;
|
|
266
|
-
return `${t.slice(0, max)}…`;
|
|
267
|
-
}
|
|
268
|
-
function scorePercent(score) {
|
|
269
|
-
return Math.round(Math.min(1, Math.max(0, score)) * 100);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
class FaqEmptyStateComponent {
|
|
273
|
-
constructor() {
|
|
274
|
-
this.starters = FAQ_STARTER_TEMPLATES;
|
|
275
|
-
this.add = new EventEmitter();
|
|
276
|
-
this.useTemplate = new EventEmitter();
|
|
277
|
-
}
|
|
278
|
-
onAdd() {
|
|
279
|
-
this.add.emit();
|
|
280
|
-
}
|
|
281
|
-
onTemplate(t) {
|
|
282
|
-
this.useTemplate.emit(t);
|
|
283
|
-
}
|
|
284
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqEmptyStateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
285
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: FaqEmptyStateComponent, isStandalone: true, selector: "eds-faq-empty-state", outputs: { add: "add", useTemplate: "useTemplate" }, ngImport: i0, template: "<div class=\"faq-empty-state\">\n <div class=\"faq-empty-state__icon\" aria-hidden=\"true\">?</div>\n <h2 class=\"eds-h2\">No FAQs yet</h2>\n <p class=\"eds-body-sm\">Add answers your AI receptionist can use on live calls.</p>\n <eds-button variant=\"primary\" (click)=\"onAdd()\">Add your first FAQ</eds-button>\n\n <p class=\"eds-label faq-empty-state__suggest\">Suggested starters</p>\n <ul class=\"faq-empty-state__starters\">\n @for (t of starters; track t.label) {\n <li>\n <button type=\"button\" class=\"faq-empty-state__starter\" (click)=\"onTemplate(t)\">\n {{ t.label }}\n </button>\n </li>\n }\n </ul>\n</div>\n", styles: [".faq-empty-state{text-align:center;padding:48px 24px}.faq-empty-state__icon{width:56px;height:56px;margin:0 auto 12px;border-radius:50%;background:#0d94881f;color:#0d9488;font-size:28px;font-weight:700;display:flex;align-items:center;justify-content:center}.faq-empty-state__suggest{margin:24px 0 8px}.faq-empty-state__starters{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:8px}.faq-empty-state__starter{border:1px dashed var(--eds-border-subtle, #d1d5db);background:var(--eds-surface-l0, #fff);border-radius:999px;padding:6px 12px;font-size:13px;cursor:pointer}.faq-empty-state__starter:hover{border-color:var(--eds-brand, #0d9488);color:var(--eds-brand, #0d9488)}\n"], dependencies: [{ kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
286
|
-
}
|
|
287
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqEmptyStateComponent, decorators: [{
|
|
288
|
-
type: Component,
|
|
289
|
-
args: [{ selector: 'eds-faq-empty-state', imports: [EdsButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"faq-empty-state\">\n <div class=\"faq-empty-state__icon\" aria-hidden=\"true\">?</div>\n <h2 class=\"eds-h2\">No FAQs yet</h2>\n <p class=\"eds-body-sm\">Add answers your AI receptionist can use on live calls.</p>\n <eds-button variant=\"primary\" (click)=\"onAdd()\">Add your first FAQ</eds-button>\n\n <p class=\"eds-label faq-empty-state__suggest\">Suggested starters</p>\n <ul class=\"faq-empty-state__starters\">\n @for (t of starters; track t.label) {\n <li>\n <button type=\"button\" class=\"faq-empty-state__starter\" (click)=\"onTemplate(t)\">\n {{ t.label }}\n </button>\n </li>\n }\n </ul>\n</div>\n", styles: [".faq-empty-state{text-align:center;padding:48px 24px}.faq-empty-state__icon{width:56px;height:56px;margin:0 auto 12px;border-radius:50%;background:#0d94881f;color:#0d9488;font-size:28px;font-weight:700;display:flex;align-items:center;justify-content:center}.faq-empty-state__suggest{margin:24px 0 8px}.faq-empty-state__starters{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:8px}.faq-empty-state__starter{border:1px dashed var(--eds-border-subtle, #d1d5db);background:var(--eds-surface-l0, #fff);border-radius:999px;padding:6px 12px;font-size:13px;cursor:pointer}.faq-empty-state__starter:hover{border-color:var(--eds-brand, #0d9488);color:var(--eds-brand, #0d9488)}\n"] }]
|
|
290
|
-
}], propDecorators: { add: [{
|
|
291
|
-
type: Output
|
|
292
|
-
}], useTemplate: [{
|
|
293
|
-
type: Output
|
|
294
|
-
}] } });
|
|
295
|
-
|
|
296
|
-
class FaqFormComponent {
|
|
297
|
-
constructor() {
|
|
298
|
-
this.open = false;
|
|
299
|
-
this.faq = null;
|
|
300
|
-
this.initialQuestion = '';
|
|
301
|
-
this.initialAnswer = '';
|
|
302
|
-
this.initialCategory = '';
|
|
303
|
-
this.closed = new EventEmitter();
|
|
304
|
-
this.saved = new EventEmitter();
|
|
305
|
-
this.categories = FAQ_CATEGORY_PRESETS;
|
|
306
|
-
this.saving = signal(false);
|
|
307
|
-
this.error = signal(null);
|
|
308
|
-
this.question = '';
|
|
309
|
-
this.answer = '';
|
|
310
|
-
this.category = '';
|
|
311
|
-
this.sortOrder = 0;
|
|
312
|
-
this.isActive = true;
|
|
313
|
-
}
|
|
314
|
-
get isEdit() {
|
|
315
|
-
return !!this.faq;
|
|
316
|
-
}
|
|
317
|
-
ngOnChanges() {
|
|
318
|
-
if (this.open)
|
|
319
|
-
this.resetForm();
|
|
320
|
-
}
|
|
321
|
-
close() {
|
|
322
|
-
this.closed.emit();
|
|
323
|
-
}
|
|
324
|
-
submit() {
|
|
325
|
-
this.error.set(null);
|
|
326
|
-
const q = this.question.trim();
|
|
327
|
-
const a = this.answer.trim();
|
|
328
|
-
if (q.length < 3 || q.length > 500) {
|
|
329
|
-
this.error.set('Question must be 3–500 characters.');
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (a.length < 3 || a.length > 2000) {
|
|
333
|
-
this.error.set('Answer must be 3–2000 characters.');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const cat = this.category.trim();
|
|
337
|
-
if (cat.length > 64) {
|
|
338
|
-
this.error.set('Category must be 64 characters or fewer.');
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
const payload = {
|
|
342
|
-
question: q,
|
|
343
|
-
answer: a,
|
|
344
|
-
category: cat || undefined,
|
|
345
|
-
sortOrder: this.sortOrder,
|
|
346
|
-
isActive: this.isActive,
|
|
347
|
-
};
|
|
348
|
-
this.saved.emit(payload);
|
|
349
|
-
}
|
|
350
|
-
resetForm() {
|
|
351
|
-
this.error.set(null);
|
|
352
|
-
this.saving.set(false);
|
|
353
|
-
if (this.faq) {
|
|
354
|
-
this.question = this.faq.question;
|
|
355
|
-
this.answer = this.faq.answer;
|
|
356
|
-
this.category = this.faq.category ?? '';
|
|
357
|
-
this.sortOrder = this.faq.sortOrder;
|
|
358
|
-
this.isActive = this.faq.isActive;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
this.question = this.initialQuestion;
|
|
362
|
-
this.answer = this.initialAnswer;
|
|
363
|
-
this.category = this.initialCategory;
|
|
364
|
-
this.sortOrder = 0;
|
|
365
|
-
this.isActive = true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
369
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: FaqFormComponent, isStandalone: true, selector: "eds-faq-form", inputs: { open: "open", faq: "faq", initialQuestion: "initialQuestion", initialAnswer: "initialAnswer", initialCategory: "initialCategory" }, outputs: { closed: "closed", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <app-modal\n [title]=\"isEdit ? 'Edit FAQ' : 'Add FAQ'\"\n subtitle=\"Short, speakable answers work best on phone calls\"\n maxWidth=\"640px\"\n (closed)=\"close()\"\n >\n <form class=\"faq-form\" (ngSubmit)=\"submit()\" novalidate>\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Question <span class=\"faq-form__req\">*</span></span>\n <span class=\"faq-form__hint\">How a caller might ask</span>\n <textarea\n class=\"faq-form__textarea\"\n rows=\"2\"\n maxlength=\"500\"\n [(ngModel)]=\"question\"\n name=\"question\"\n required\n ></textarea>\n <span class=\"faq-form__count\">{{ question.length }}/500</span>\n </label>\n\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Answer <span class=\"faq-form__req\">*</span></span>\n <span class=\"faq-form__hint\">Write how you want the AI to speak on the phone \u2014 short, clear, no bullet points.</span>\n <textarea\n class=\"faq-form__textarea\"\n rows=\"4\"\n maxlength=\"2000\"\n [(ngModel)]=\"answer\"\n name=\"answer\"\n required\n ></textarea>\n <span class=\"faq-form__count\">{{ answer.length }}/2000</span>\n </label>\n\n <div class=\"faq-form__row\">\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Category</span>\n <select class=\"faq-form__input\" [(ngModel)]=\"category\" name=\"category\">\n <option value=\"\">\u2014 None \u2014</option>\n @for (c of categories; track c) {\n <option [value]=\"c\">{{ c }}</option>\n }\n </select>\n </label>\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Sort order</span>\n <input type=\"number\" class=\"faq-form__input\" [(ngModel)]=\"sortOrder\" name=\"sortOrder\" min=\"0\" />\n </label>\n </div>\n\n <label class=\"faq-form__check\">\n <input type=\"checkbox\" [(ngModel)]=\"isActive\" name=\"isActive\" />\n Active (voice agent can use this answer)\n </label>\n\n <section class=\"faq-form__preview eds-card\">\n <h3 class=\"eds-label\">Live preview</h3>\n <p class=\"faq-form__preview-line\"><strong>Caller asks:</strong> \u201C{{ question || '\u2026' }}\u201D</p>\n <p class=\"faq-form__preview-line\"><strong>AI would say:</strong> \u201C{{ answer || '\u2026' }}\u201D</p>\n </section>\n\n @if (error()) {\n <p class=\"faq-form__error\" role=\"alert\">{{ error() }}</p>\n }\n\n <footer class=\"faq-form__footer\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"close()\">Cancel</eds-button>\n <eds-button variant=\"primary\" type=\"submit\">Save</eds-button>\n </footer>\n </form>\n </app-modal>\n}\n", styles: [".faq-form{display:flex;flex-direction:column;gap:14px}.faq-form__field{display:flex;flex-direction:column;gap:4px}.faq-form__hint{font-size:12px;color:var(--eds-text-secondary)}.faq-form__textarea,.faq-form__input{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-form__textarea{resize:vertical}.faq-form__count{font-size:11px;color:var(--eds-text-secondary);text-align:right}.faq-form__req{color:#dc2626}.faq-form__row{display:grid;grid-template-columns:1fr 120px;gap:12px}.faq-form__check{display:flex;align-items:center;gap:8px;font-size:13px}.faq-form__preview{padding:12px;background:var(--eds-surface-muted, #f8fafc)}.faq-form__preview-line{margin:6px 0 0;font-size:14px;line-height:1.5}.faq-form__error{color:#b91c1c;font-size:13px;margin:0}.faq-form__footer{display:flex;justify-content:flex-end;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
370
|
-
}
|
|
371
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqFormComponent, decorators: [{
|
|
372
|
-
type: Component,
|
|
373
|
-
args: [{ selector: 'eds-faq-form', imports: [FormsModule, ModalComponent, EdsButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <app-modal\n [title]=\"isEdit ? 'Edit FAQ' : 'Add FAQ'\"\n subtitle=\"Short, speakable answers work best on phone calls\"\n maxWidth=\"640px\"\n (closed)=\"close()\"\n >\n <form class=\"faq-form\" (ngSubmit)=\"submit()\" novalidate>\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Question <span class=\"faq-form__req\">*</span></span>\n <span class=\"faq-form__hint\">How a caller might ask</span>\n <textarea\n class=\"faq-form__textarea\"\n rows=\"2\"\n maxlength=\"500\"\n [(ngModel)]=\"question\"\n name=\"question\"\n required\n ></textarea>\n <span class=\"faq-form__count\">{{ question.length }}/500</span>\n </label>\n\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Answer <span class=\"faq-form__req\">*</span></span>\n <span class=\"faq-form__hint\">Write how you want the AI to speak on the phone \u2014 short, clear, no bullet points.</span>\n <textarea\n class=\"faq-form__textarea\"\n rows=\"4\"\n maxlength=\"2000\"\n [(ngModel)]=\"answer\"\n name=\"answer\"\n required\n ></textarea>\n <span class=\"faq-form__count\">{{ answer.length }}/2000</span>\n </label>\n\n <div class=\"faq-form__row\">\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Category</span>\n <select class=\"faq-form__input\" [(ngModel)]=\"category\" name=\"category\">\n <option value=\"\">\u2014 None \u2014</option>\n @for (c of categories; track c) {\n <option [value]=\"c\">{{ c }}</option>\n }\n </select>\n </label>\n <label class=\"faq-form__field\">\n <span class=\"eds-label\">Sort order</span>\n <input type=\"number\" class=\"faq-form__input\" [(ngModel)]=\"sortOrder\" name=\"sortOrder\" min=\"0\" />\n </label>\n </div>\n\n <label class=\"faq-form__check\">\n <input type=\"checkbox\" [(ngModel)]=\"isActive\" name=\"isActive\" />\n Active (voice agent can use this answer)\n </label>\n\n <section class=\"faq-form__preview eds-card\">\n <h3 class=\"eds-label\">Live preview</h3>\n <p class=\"faq-form__preview-line\"><strong>Caller asks:</strong> \u201C{{ question || '\u2026' }}\u201D</p>\n <p class=\"faq-form__preview-line\"><strong>AI would say:</strong> \u201C{{ answer || '\u2026' }}\u201D</p>\n </section>\n\n @if (error()) {\n <p class=\"faq-form__error\" role=\"alert\">{{ error() }}</p>\n }\n\n <footer class=\"faq-form__footer\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"close()\">Cancel</eds-button>\n <eds-button variant=\"primary\" type=\"submit\">Save</eds-button>\n </footer>\n </form>\n </app-modal>\n}\n", styles: [".faq-form{display:flex;flex-direction:column;gap:14px}.faq-form__field{display:flex;flex-direction:column;gap:4px}.faq-form__hint{font-size:12px;color:var(--eds-text-secondary)}.faq-form__textarea,.faq-form__input{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-form__textarea{resize:vertical}.faq-form__count{font-size:11px;color:var(--eds-text-secondary);text-align:right}.faq-form__req{color:#dc2626}.faq-form__row{display:grid;grid-template-columns:1fr 120px;gap:12px}.faq-form__check{display:flex;align-items:center;gap:8px;font-size:13px}.faq-form__preview{padding:12px;background:var(--eds-surface-muted, #f8fafc)}.faq-form__preview-line{margin:6px 0 0;font-size:14px;line-height:1.5}.faq-form__error{color:#b91c1c;font-size:13px;margin:0}.faq-form__footer{display:flex;justify-content:flex-end;gap:8px}\n"] }]
|
|
374
|
-
}], propDecorators: { open: [{
|
|
375
|
-
type: Input
|
|
376
|
-
}], faq: [{
|
|
377
|
-
type: Input
|
|
378
|
-
}], initialQuestion: [{
|
|
379
|
-
type: Input
|
|
380
|
-
}], initialAnswer: [{
|
|
381
|
-
type: Input
|
|
382
|
-
}], initialCategory: [{
|
|
383
|
-
type: Input
|
|
384
|
-
}], closed: [{
|
|
385
|
-
type: Output
|
|
386
|
-
}], saved: [{
|
|
387
|
-
type: Output
|
|
388
|
-
}] } });
|
|
389
|
-
|
|
390
|
-
class FaqSearchTesterComponent {
|
|
391
|
-
constructor() {
|
|
392
|
-
this.api = inject(PracticeFaqApiService);
|
|
393
|
-
this.query = '';
|
|
394
|
-
this.loading = signal(false);
|
|
395
|
-
this.searched = signal(false);
|
|
396
|
-
this.found = signal(false);
|
|
397
|
-
this.results = signal([]);
|
|
398
|
-
this.error = signal(null);
|
|
399
|
-
this.categoryClass = categoryBadgeClass;
|
|
400
|
-
this.scorePercent = scorePercent;
|
|
401
|
-
}
|
|
402
|
-
search() {
|
|
403
|
-
const q = this.query.trim();
|
|
404
|
-
if (!q)
|
|
405
|
-
return;
|
|
406
|
-
this.loading.set(true);
|
|
407
|
-
this.error.set(null);
|
|
408
|
-
this.searched.set(false);
|
|
409
|
-
this.api.search(q, 3).subscribe({
|
|
410
|
-
next: (res) => {
|
|
411
|
-
this.loading.set(false);
|
|
412
|
-
this.searched.set(true);
|
|
413
|
-
this.found.set(res.found);
|
|
414
|
-
this.results.set(res.items);
|
|
415
|
-
},
|
|
416
|
-
error: () => {
|
|
417
|
-
this.loading.set(false);
|
|
418
|
-
this.error.set('Search failed. Please try again.');
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqSearchTesterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
423
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: FaqSearchTesterComponent, isStandalone: true, selector: "eds-faq-search-tester", ngImport: i0, template: "<aside class=\"eds-card faq-search-tester\">\n <h2 class=\"eds-h2\">Test voice search</h2>\n <p class=\"eds-body-sm faq-search-tester__help\">This uses the same search as live calls.</p>\n\n <label class=\"faq-search-tester__field\">\n <span class=\"eds-label\">What a caller might ask\u2026</span>\n <input\n type=\"search\"\n class=\"faq-search-tester__input\"\n placeholder=\"Do you have Saturday hours?\"\n [(ngModel)]=\"query\"\n (keyup.enter)=\"search()\"\n />\n </label>\n\n <eds-button variant=\"primary\" [disabled]=\"loading() || !query.trim()\" (click)=\"search()\">\n {{ loading() ? 'Searching\u2026' : 'Test search' }}\n </eds-button>\n\n @if (error()) {\n <p class=\"faq-search-tester__error\" role=\"alert\">{{ error() }}</p>\n }\n\n @if (searched()) {\n @if (found()) {\n <ul class=\"faq-search-tester__results\">\n @for (item of results(); track item.faqId) {\n <li class=\"faq-search-tester__result\">\n <div class=\"faq-search-tester__score-row\">\n <span class=\"eds-label\">Relevance</span>\n <div class=\"faq-search-tester__score-bar\">\n <span class=\"faq-search-tester__score-fill\" [style.width.%]=\"scorePercent(item.score)\"></span>\n </div>\n <span class=\"eds-mono-sm\">{{ scorePercent(item.score) }}%</span>\n </div>\n @if (item.category) {\n <span class=\"faq-chip\" [class]=\"categoryClass(item.category)\">{{ item.category }}</span>\n }\n <p class=\"faq-search-tester__question\">{{ item.question }}</p>\n <p class=\"faq-search-tester__answer\">{{ item.answer }}</p>\n </li>\n }\n </ul>\n } @else {\n <p class=\"faq-search-tester__no-match\" role=\"status\">\n No match \u2014 voice agent will offer to transfer to reception\n </p>\n }\n }\n</aside>\n", styles: [".faq-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize}.faq-chip--hours{background:#3b82f61f;color:#2563eb}.faq-chip--location{background:#14b8a61f;color:#0d9488}.faq-chip--parking{background:#6b72801f;color:#4b5563}.faq-chip--insurance{background:#8b5cf61f;color:#7c3aed}.faq-chip--services{background:#22c55e1f;color:#16a34a}.faq-chip--policies{background:#f973161f;color:#ea580c}.faq-chip--general{background:#6b72801f;color:#6b7280}.faq-search-tester{padding:16px;position:sticky;top:calc(var(--eds-topbar-height, 56px) + 16px)}.faq-search-tester__help{color:var(--eds-text-secondary);margin:4px 0 12px}.faq-search-tester__field{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}.faq-search-tester__input{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-search-tester__error{color:#b91c1c;font-size:13px;margin-top:10px}.faq-search-tester__no-match{margin-top:14px;padding:12px;border-radius:8px;background:#f59e0b1f;color:#b45309;font-size:13px}.faq-search-tester__results{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:12px}.faq-search-tester__result{padding:12px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px}.faq-search-tester__score-row{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;margin-bottom:8px}.faq-search-tester__score-bar{height:6px;border-radius:999px;background:var(--eds-surface-muted, #e5e7eb);overflow:hidden}.faq-search-tester__score-fill{display:block;height:100%;background:var(--eds-brand, #0d9488);border-radius:999px}.faq-search-tester__question{font-weight:600;font-size:13px;margin:6px 0 4px}.faq-search-tester__answer{font-size:13px;color:var(--eds-text-secondary);margin:0;line-height:1.5}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
424
|
-
}
|
|
425
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqSearchTesterComponent, decorators: [{
|
|
426
|
-
type: Component,
|
|
427
|
-
args: [{ selector: 'eds-faq-search-tester', imports: [FormsModule, EdsButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<aside class=\"eds-card faq-search-tester\">\n <h2 class=\"eds-h2\">Test voice search</h2>\n <p class=\"eds-body-sm faq-search-tester__help\">This uses the same search as live calls.</p>\n\n <label class=\"faq-search-tester__field\">\n <span class=\"eds-label\">What a caller might ask\u2026</span>\n <input\n type=\"search\"\n class=\"faq-search-tester__input\"\n placeholder=\"Do you have Saturday hours?\"\n [(ngModel)]=\"query\"\n (keyup.enter)=\"search()\"\n />\n </label>\n\n <eds-button variant=\"primary\" [disabled]=\"loading() || !query.trim()\" (click)=\"search()\">\n {{ loading() ? 'Searching\u2026' : 'Test search' }}\n </eds-button>\n\n @if (error()) {\n <p class=\"faq-search-tester__error\" role=\"alert\">{{ error() }}</p>\n }\n\n @if (searched()) {\n @if (found()) {\n <ul class=\"faq-search-tester__results\">\n @for (item of results(); track item.faqId) {\n <li class=\"faq-search-tester__result\">\n <div class=\"faq-search-tester__score-row\">\n <span class=\"eds-label\">Relevance</span>\n <div class=\"faq-search-tester__score-bar\">\n <span class=\"faq-search-tester__score-fill\" [style.width.%]=\"scorePercent(item.score)\"></span>\n </div>\n <span class=\"eds-mono-sm\">{{ scorePercent(item.score) }}%</span>\n </div>\n @if (item.category) {\n <span class=\"faq-chip\" [class]=\"categoryClass(item.category)\">{{ item.category }}</span>\n }\n <p class=\"faq-search-tester__question\">{{ item.question }}</p>\n <p class=\"faq-search-tester__answer\">{{ item.answer }}</p>\n </li>\n }\n </ul>\n } @else {\n <p class=\"faq-search-tester__no-match\" role=\"status\">\n No match \u2014 voice agent will offer to transfer to reception\n </p>\n }\n }\n</aside>\n", styles: [".faq-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize}.faq-chip--hours{background:#3b82f61f;color:#2563eb}.faq-chip--location{background:#14b8a61f;color:#0d9488}.faq-chip--parking{background:#6b72801f;color:#4b5563}.faq-chip--insurance{background:#8b5cf61f;color:#7c3aed}.faq-chip--services{background:#22c55e1f;color:#16a34a}.faq-chip--policies{background:#f973161f;color:#ea580c}.faq-chip--general{background:#6b72801f;color:#6b7280}.faq-search-tester{padding:16px;position:sticky;top:calc(var(--eds-topbar-height, 56px) + 16px)}.faq-search-tester__help{color:var(--eds-text-secondary);margin:4px 0 12px}.faq-search-tester__field{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}.faq-search-tester__input{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-search-tester__error{color:#b91c1c;font-size:13px;margin-top:10px}.faq-search-tester__no-match{margin-top:14px;padding:12px;border-radius:8px;background:#f59e0b1f;color:#b45309;font-size:13px}.faq-search-tester__results{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:12px}.faq-search-tester__result{padding:12px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px}.faq-search-tester__score-row{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;margin-bottom:8px}.faq-search-tester__score-bar{height:6px;border-radius:999px;background:var(--eds-surface-muted, #e5e7eb);overflow:hidden}.faq-search-tester__score-fill{display:block;height:100%;background:var(--eds-brand, #0d9488);border-radius:999px}.faq-search-tester__question{font-weight:600;font-size:13px;margin:6px 0 4px}.faq-search-tester__answer{font-size:13px;color:var(--eds-text-secondary);margin:0;line-height:1.5}\n"] }]
|
|
428
|
-
}] });
|
|
429
|
-
|
|
430
|
-
class KnowledgeHubPageComponent {
|
|
431
|
-
constructor() {
|
|
432
|
-
this.api = inject(PracticeFaqApiService);
|
|
433
|
-
this.toast = inject(ToastService);
|
|
434
|
-
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
435
|
-
this.pageFaqs = signal([]);
|
|
436
|
-
this.total = signal(0);
|
|
437
|
-
this.page = signal(1);
|
|
438
|
-
this.loading = signal(true);
|
|
439
|
-
this.error = signal(null);
|
|
440
|
-
this.activeFilter = signal('all');
|
|
441
|
-
this.categoryFilter = signal('');
|
|
442
|
-
this.listSearch = signal('');
|
|
443
|
-
this.formOpen = signal(false);
|
|
444
|
-
this.editing = signal(null);
|
|
445
|
-
this.formSeed = signal({
|
|
446
|
-
question: '',
|
|
447
|
-
answer: '',
|
|
448
|
-
category: '',
|
|
449
|
-
});
|
|
450
|
-
this.categories = FAQ_CATEGORY_PRESETS;
|
|
451
|
-
this.activeTabs = [
|
|
452
|
-
{ id: 'all', label: 'All' },
|
|
453
|
-
{ id: 'active', label: 'Active only' },
|
|
454
|
-
{ id: 'inactive', label: 'Inactive' },
|
|
455
|
-
];
|
|
456
|
-
/** Client filters still applied on the current page (category / local search / inactive). */
|
|
457
|
-
this.faqs = computed(() => {
|
|
458
|
-
let rows = [...this.pageFaqs()];
|
|
459
|
-
const active = this.activeFilter();
|
|
460
|
-
if (active === 'inactive')
|
|
461
|
-
rows = rows.filter((f) => !f.isActive);
|
|
462
|
-
const cat = this.categoryFilter();
|
|
463
|
-
if (cat)
|
|
464
|
-
rows = rows.filter((f) => (f.category ?? '') === cat);
|
|
465
|
-
const q = this.listSearch().trim().toLowerCase();
|
|
466
|
-
if (q) {
|
|
467
|
-
rows = rows.filter((f) => f.question.toLowerCase().includes(q) ||
|
|
468
|
-
f.answer.toLowerCase().includes(q) ||
|
|
469
|
-
(f.category?.toLowerCase().includes(q) ?? false));
|
|
470
|
-
}
|
|
471
|
-
return rows.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
472
|
-
});
|
|
473
|
-
this.categoryClass = categoryBadgeClass;
|
|
474
|
-
this.truncate = truncateText;
|
|
475
|
-
}
|
|
476
|
-
ngOnInit() {
|
|
477
|
-
this.load();
|
|
478
|
-
}
|
|
479
|
-
load() {
|
|
480
|
-
this.loading.set(true);
|
|
481
|
-
this.error.set(null);
|
|
482
|
-
this.api
|
|
483
|
-
.list({
|
|
484
|
-
activeOnly: this.activeFilter() === 'active',
|
|
485
|
-
page: this.page(),
|
|
486
|
-
limit: DEFAULT_PAGE_SIZE,
|
|
487
|
-
})
|
|
488
|
-
.subscribe({
|
|
489
|
-
next: ({ items, total, page }) => {
|
|
490
|
-
this.pageFaqs.set(items);
|
|
491
|
-
this.total.set(total);
|
|
492
|
-
this.page.set(page);
|
|
493
|
-
this.loading.set(false);
|
|
494
|
-
},
|
|
495
|
-
error: () => {
|
|
496
|
-
this.loading.set(false);
|
|
497
|
-
this.error.set('Failed to load FAQs.');
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
setActiveFilter(id) {
|
|
502
|
-
this.activeFilter.set(id);
|
|
503
|
-
this.page.set(1);
|
|
504
|
-
this.load();
|
|
505
|
-
}
|
|
506
|
-
toggleCategory(cat) {
|
|
507
|
-
this.categoryFilter.update((c) => (c === cat ? '' : cat));
|
|
508
|
-
}
|
|
509
|
-
isCategorySelected(cat) {
|
|
510
|
-
return this.categoryFilter() === cat;
|
|
511
|
-
}
|
|
512
|
-
totalPages() {
|
|
513
|
-
return totalPages(this.total(), DEFAULT_PAGE_SIZE);
|
|
514
|
-
}
|
|
515
|
-
prevPage() {
|
|
516
|
-
if (this.page() <= 1)
|
|
517
|
-
return;
|
|
518
|
-
this.page.update((p) => p - 1);
|
|
519
|
-
this.load();
|
|
520
|
-
}
|
|
521
|
-
nextPage() {
|
|
522
|
-
if (this.page() >= this.totalPages())
|
|
523
|
-
return;
|
|
524
|
-
this.page.update((p) => p + 1);
|
|
525
|
-
this.load();
|
|
526
|
-
}
|
|
527
|
-
openCreate() {
|
|
528
|
-
this.editing.set(null);
|
|
529
|
-
this.formSeed.set({ question: '', answer: '', category: '' });
|
|
530
|
-
this.formOpen.set(true);
|
|
531
|
-
}
|
|
532
|
-
openEdit(faq) {
|
|
533
|
-
this.editing.set(faq);
|
|
534
|
-
this.formOpen.set(true);
|
|
535
|
-
}
|
|
536
|
-
closeForm() {
|
|
537
|
-
this.formOpen.set(false);
|
|
538
|
-
this.editing.set(null);
|
|
539
|
-
}
|
|
540
|
-
onStarterTemplate(t) {
|
|
541
|
-
this.editing.set(null);
|
|
542
|
-
this.formSeed.set({ question: t.question, answer: t.answer, category: t.category });
|
|
543
|
-
this.formOpen.set(true);
|
|
544
|
-
}
|
|
545
|
-
onSave(payload) {
|
|
546
|
-
const edit = this.editing();
|
|
547
|
-
if (edit) {
|
|
548
|
-
this.api.update(edit.faqId, payload).subscribe({
|
|
549
|
-
next: () => {
|
|
550
|
-
this.closeForm();
|
|
551
|
-
this.toast.show('FAQ saved — voice agent updated', 'success');
|
|
552
|
-
this.load();
|
|
553
|
-
},
|
|
554
|
-
error: () => this.toast.show('Failed to save FAQ', 'error'),
|
|
555
|
-
});
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
this.api.create(payload).subscribe({
|
|
559
|
-
next: () => {
|
|
560
|
-
this.closeForm();
|
|
561
|
-
this.toast.show('Your AI receptionist can now answer this on live calls.', 'success');
|
|
562
|
-
this.page.set(1);
|
|
563
|
-
this.load();
|
|
564
|
-
},
|
|
565
|
-
error: () => this.toast.show('Failed to create FAQ', 'error'),
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
toggleActive(faq, active) {
|
|
569
|
-
this.pageFaqs.update((list) => list.map((f) => (f.faqId === faq.faqId ? { ...f, isActive: active } : f)));
|
|
570
|
-
this.api.update(faq.faqId, { isActive: active }).subscribe({
|
|
571
|
-
next: (updated) => {
|
|
572
|
-
this.pageFaqs.update((list) => list.map((f) => (f.faqId === updated.faqId ? updated : f)));
|
|
573
|
-
},
|
|
574
|
-
error: () => {
|
|
575
|
-
this.pageFaqs.update((list) => list.map((f) => (f.faqId === faq.faqId ? { ...f, isActive: !active } : f)));
|
|
576
|
-
this.toast.show('Failed to update FAQ status', 'error');
|
|
577
|
-
},
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
deleteFaq(faq) {
|
|
581
|
-
const msg = 'Remove this FAQ from the knowledge base? The voice agent will no longer use it.';
|
|
582
|
-
if (!confirm(msg))
|
|
583
|
-
return;
|
|
584
|
-
this.api.delete(faq.faqId).subscribe({
|
|
585
|
-
next: () => {
|
|
586
|
-
this.toast.show('FAQ removed', 'success');
|
|
587
|
-
this.load();
|
|
588
|
-
},
|
|
589
|
-
error: () => this.toast.show('Failed to delete FAQ', 'error'),
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeHubPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
593
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: KnowledgeHubPageComponent, isStandalone: true, selector: "eds-knowledge-hub-page", ngImport: i0, template: "<div class=\"eds-page knowledge-hub\">\n <header class=\"knowledge-hub__header\">\n <div>\n <h1 class=\"eds-h1\">Practice knowledge base</h1>\n <p class=\"eds-body-sm\">\n Answers here are used by your AI receptionist on phone calls. Keep replies short and factual.\n </p>\n </div>\n <eds-button variant=\"primary\" class=\"knowledge-hub__add-desktop\" (click)=\"openCreate()\">\n <eds-icon name=\"add\" [size]=\"18\" /> Add FAQ\n </eds-button>\n </header>\n\n <div class=\"knowledge-hub__banner eds-card\" role=\"note\">\n <eds-icon name=\"info\" [size]=\"18\" />\n <p class=\"eds-body-sm\">\n Do not put patient-specific or clinical advice here. Use general practice information only.\n </p>\n </div>\n\n <div class=\"knowledge-hub__layout\">\n <main class=\"knowledge-hub__main\">\n <nav class=\"knowledge-hub__tabs\" aria-label=\"FAQ status\">\n @for (t of activeTabs; track t.id) {\n <button\n type=\"button\"\n class=\"knowledge-hub__tab\"\n [class.knowledge-hub__tab--active]=\"activeFilter() === t.id\"\n (click)=\"setActiveFilter(t.id)\"\n >\n {{ t.label }}\n </button>\n }\n </nav>\n\n <div class=\"knowledge-hub__filters eds-card\">\n <input\n type=\"search\"\n class=\"knowledge-hub__search\"\n placeholder=\"Filter by question or answer\u2026\"\n [ngModel]=\"listSearch()\"\n (ngModelChange)=\"listSearch.set($event)\"\n />\n <div class=\"knowledge-hub__chips\">\n @for (cat of categories; track cat) {\n <button\n type=\"button\"\n class=\"faq-chip knowledge-hub__chip\"\n [class]=\"'faq-chip--' + cat\"\n [class.knowledge-hub__chip--on]=\"isCategorySelected(cat)\"\n (click)=\"toggleCategory(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n </div>\n\n @if (loading()) {\n <div class=\"knowledge-hub__skeleton eds-card\">\n @for (i of [1, 2, 3]; track i) {\n <div class=\"knowledge-hub__skel-row\"></div>\n }\n </div>\n } @else if (error()) {\n <p class=\"knowledge-hub__error eds-body-sm\" role=\"alert\">{{ error() }}</p>\n <eds-button variant=\"secondary\" (click)=\"load()\">Retry</eds-button>\n } @else if (total() === 0 && !categoryFilter() && !listSearch() && activeFilter() === 'all') {\n <div class=\"eds-card\">\n <eds-faq-empty-state (add)=\"openCreate()\" (useTemplate)=\"onStarterTemplate($event)\" />\n </div>\n } @else if (faqs().length === 0) {\n <p class=\"eds-body-sm knowledge-hub__empty-filter\">No FAQs match your filters.</p>\n } @else {\n <div class=\"eds-card knowledge-hub__table-wrap\">\n <table class=\"eds-table knowledge-hub__table\">\n <thead>\n <tr>\n <th>Question</th>\n <th>Answer</th>\n <th>Category</th>\n <th>Active</th>\n <th>Updated</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (faq of faqs(); track faq.faqId) {\n <tr [class.knowledge-hub__row--inactive]=\"!faq.isActive\">\n <td class=\"knowledge-hub__question\" [title]=\"faq.question\">{{ faq.question }}</td>\n <td class=\"knowledge-hub__answer\" [title]=\"faq.answer\">{{ truncate(faq.answer, 72) }}</td>\n <td>\n @if (faq.category) {\n <span class=\"faq-chip\" [class]=\"categoryClass(faq.category)\">{{ faq.category }}</span>\n } @else {\n <span class=\"eds-body-sm\">\u2014</span>\n }\n </td>\n <td>\n <label class=\"knowledge-hub__toggle\">\n <input\n type=\"checkbox\"\n [checked]=\"faq.isActive\"\n (change)=\"toggleActive(faq, $any($event.target).checked)\"\n [attr.aria-label]=\"'Toggle active for ' + faq.question\"\n />\n <span class=\"knowledge-hub__status\" [class.knowledge-hub__status--on]=\"faq.isActive\">\n {{ faq.isActive ? 'Active' : 'Inactive' }}\n </span>\n </label>\n </td>\n <td class=\"eds-mono-sm\">{{ faq.updatedAt | date: 'mediumDate' }}</td>\n <td class=\"knowledge-hub__actions\">\n <button type=\"button\" class=\"knowledge-hub__link\" (click)=\"openEdit(faq)\">Edit</button>\n <button type=\"button\" class=\"knowledge-hub__link knowledge-hub__link--danger\" (click)=\"deleteFaq(faq)\">\n Delete\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <footer class=\"knowledge-hub__pager\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"knowledge-hub__pager-actions\">\n <eds-button variant=\"secondary\" [disabled]=\"page() <= 1\" (click)=\"prevPage()\">Previous</eds-button>\n <eds-button variant=\"secondary\" [disabled]=\"page() >= totalPages()\" (click)=\"nextPage()\">Next</eds-button>\n </div>\n </footer>\n }\n </main>\n\n <eds-faq-search-tester />\n </div>\n\n <button type=\"button\" class=\"knowledge-hub__fab\" aria-label=\"Add FAQ\" (click)=\"openCreate()\">\n <eds-icon name=\"add\" [size]=\"24\" />\n </button>\n\n <eds-faq-form\n [open]=\"formOpen()\"\n [faq]=\"editing()\"\n [initialQuestion]=\"formSeed().question\"\n [initialAnswer]=\"formSeed().answer\"\n [initialCategory]=\"formSeed().category\"\n (closed)=\"closeForm()\"\n (saved)=\"onSave($event)\"\n />\n</div>\n", styles: [".faq-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize}.faq-chip--hours{background:#3b82f61f;color:#2563eb}.faq-chip--location{background:#14b8a61f;color:#0d9488}.faq-chip--parking{background:#6b72801f;color:#4b5563}.faq-chip--insurance{background:#8b5cf61f;color:#7c3aed}.faq-chip--services{background:#22c55e1f;color:#16a34a}.faq-chip--policies{background:#f973161f;color:#ea580c}.faq-chip--general{background:#6b72801f;color:#6b7280}.knowledge-hub__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}.knowledge-hub__banner{display:flex;align-items:flex-start;gap:10px;padding:12px 16px;margin-bottom:20px;background:#3b82f60f;border-color:#3b82f633}.knowledge-hub__banner p{margin:0}.knowledge-hub__layout{display:grid;grid-template-columns:1fr 320px;gap:20px;align-items:start}.knowledge-hub__tabs{display:flex;gap:4px;margin-bottom:12px;padding:4px;width:fit-content;background:var(--eds-surface-l1);border-radius:var(--eds-radius-md)}.knowledge-hub__tab{border:none;background:transparent;padding:8px 14px;border-radius:var(--eds-radius-sm);font-size:13px;cursor:pointer;color:var(--eds-text-secondary)}.knowledge-hub__tab--active{background:var(--eds-surface-l0);color:var(--eds-text-primary);font-weight:600;box-shadow:0 1px 2px #0000000f}.knowledge-hub__filters{padding:12px;margin-bottom:12px}.knowledge-hub__search{width:100%;padding:8px 12px;border:1px solid var(--eds-border-default);border-radius:var(--eds-radius-md);font-size:14px;margin-bottom:10px}.knowledge-hub__chips{display:flex;flex-wrap:wrap;gap:6px}.knowledge-hub__chip{border:1px solid transparent;cursor:pointer;opacity:.6}.knowledge-hub__chip--on{opacity:1;border-color:currentColor}.knowledge-hub__table-wrap{overflow-x:auto;padding:0}.knowledge-hub__question{font-weight:600;max-width:220px}.knowledge-hub__answer{color:var(--eds-text-secondary);max-width:260px}.knowledge-hub__row--inactive{opacity:.65}.knowledge-hub__toggle{display:flex;align-items:center;gap:6px;cursor:pointer}.knowledge-hub__status{font-size:12px;color:var(--eds-text-secondary)}.knowledge-hub__status--on{color:#16a34a}.knowledge-hub__status:before{content:\"\";display:inline-block;width:6px;height:6px;border-radius:50%;background:#9ca3af;margin-right:4px;vertical-align:middle}.knowledge-hub__status--on:before{background:#22c55e}.knowledge-hub__actions{white-space:nowrap;display:flex;gap:8px}.knowledge-hub__link{border:none;background:none;color:var(--eds-brand, #0d9488);font-size:13px;cursor:pointer;padding:0}.knowledge-hub__link--danger{color:#dc2626}.knowledge-hub__error{color:#b91c1c}.knowledge-hub__empty-filter{padding:24px;text-align:center;color:var(--eds-text-secondary)}.knowledge-hub__skeleton{padding:16px}.knowledge-hub__skel-row{height:48px;border-radius:8px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb,#f3f4f6 75%);background-size:200% 100%;animation:faq-shimmer 1.2s infinite;margin-bottom:8px}@keyframes faq-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.knowledge-hub__fab{display:none;position:fixed;right:24px;bottom:24px;width:56px;height:56px;border-radius:50%;border:none;background:var(--eds-brand, #0d9488);color:#fff;box-shadow:0 4px 14px #0003;cursor:pointer;align-items:center;justify-content:center}@media(max-width:960px){.knowledge-hub__layout{grid-template-columns:1fr}.knowledge-hub__add-desktop{display:none}.knowledge-hub__fab{display:inline-flex}}@media(min-width:961px){.knowledge-hub__fab{display:none}}.knowledge-hub__pager{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--eds-border-subtle, #e4e4e7)}.knowledge-hub__pager-actions{display:flex;gap:8px}\n"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "ngmodule", type: FormsModule }, { 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.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: FaqEmptyStateComponent, selector: "eds-faq-empty-state", outputs: ["add", "useTemplate"] }, { kind: "component", type: FaqFormComponent, selector: "eds-faq-form", inputs: ["open", "faq", "initialQuestion", "initialAnswer", "initialCategory"], outputs: ["closed", "saved"] }, { kind: "component", type: FaqSearchTesterComponent, selector: "eds-faq-search-tester" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
594
|
-
}
|
|
595
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeHubPageComponent, decorators: [{
|
|
596
|
-
type: Component,
|
|
597
|
-
args: [{ selector: 'eds-knowledge-hub-page', imports: [
|
|
598
|
-
DatePipe,
|
|
599
|
-
FormsModule,
|
|
600
|
-
EdsButtonComponent,
|
|
601
|
-
EdsIconComponent,
|
|
602
|
-
FaqEmptyStateComponent,
|
|
603
|
-
FaqFormComponent,
|
|
604
|
-
FaqSearchTesterComponent,
|
|
605
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page knowledge-hub\">\n <header class=\"knowledge-hub__header\">\n <div>\n <h1 class=\"eds-h1\">Practice knowledge base</h1>\n <p class=\"eds-body-sm\">\n Answers here are used by your AI receptionist on phone calls. Keep replies short and factual.\n </p>\n </div>\n <eds-button variant=\"primary\" class=\"knowledge-hub__add-desktop\" (click)=\"openCreate()\">\n <eds-icon name=\"add\" [size]=\"18\" /> Add FAQ\n </eds-button>\n </header>\n\n <div class=\"knowledge-hub__banner eds-card\" role=\"note\">\n <eds-icon name=\"info\" [size]=\"18\" />\n <p class=\"eds-body-sm\">\n Do not put patient-specific or clinical advice here. Use general practice information only.\n </p>\n </div>\n\n <div class=\"knowledge-hub__layout\">\n <main class=\"knowledge-hub__main\">\n <nav class=\"knowledge-hub__tabs\" aria-label=\"FAQ status\">\n @for (t of activeTabs; track t.id) {\n <button\n type=\"button\"\n class=\"knowledge-hub__tab\"\n [class.knowledge-hub__tab--active]=\"activeFilter() === t.id\"\n (click)=\"setActiveFilter(t.id)\"\n >\n {{ t.label }}\n </button>\n }\n </nav>\n\n <div class=\"knowledge-hub__filters eds-card\">\n <input\n type=\"search\"\n class=\"knowledge-hub__search\"\n placeholder=\"Filter by question or answer\u2026\"\n [ngModel]=\"listSearch()\"\n (ngModelChange)=\"listSearch.set($event)\"\n />\n <div class=\"knowledge-hub__chips\">\n @for (cat of categories; track cat) {\n <button\n type=\"button\"\n class=\"faq-chip knowledge-hub__chip\"\n [class]=\"'faq-chip--' + cat\"\n [class.knowledge-hub__chip--on]=\"isCategorySelected(cat)\"\n (click)=\"toggleCategory(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n </div>\n\n @if (loading()) {\n <div class=\"knowledge-hub__skeleton eds-card\">\n @for (i of [1, 2, 3]; track i) {\n <div class=\"knowledge-hub__skel-row\"></div>\n }\n </div>\n } @else if (error()) {\n <p class=\"knowledge-hub__error eds-body-sm\" role=\"alert\">{{ error() }}</p>\n <eds-button variant=\"secondary\" (click)=\"load()\">Retry</eds-button>\n } @else if (total() === 0 && !categoryFilter() && !listSearch() && activeFilter() === 'all') {\n <div class=\"eds-card\">\n <eds-faq-empty-state (add)=\"openCreate()\" (useTemplate)=\"onStarterTemplate($event)\" />\n </div>\n } @else if (faqs().length === 0) {\n <p class=\"eds-body-sm knowledge-hub__empty-filter\">No FAQs match your filters.</p>\n } @else {\n <div class=\"eds-card knowledge-hub__table-wrap\">\n <table class=\"eds-table knowledge-hub__table\">\n <thead>\n <tr>\n <th>Question</th>\n <th>Answer</th>\n <th>Category</th>\n <th>Active</th>\n <th>Updated</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (faq of faqs(); track faq.faqId) {\n <tr [class.knowledge-hub__row--inactive]=\"!faq.isActive\">\n <td class=\"knowledge-hub__question\" [title]=\"faq.question\">{{ faq.question }}</td>\n <td class=\"knowledge-hub__answer\" [title]=\"faq.answer\">{{ truncate(faq.answer, 72) }}</td>\n <td>\n @if (faq.category) {\n <span class=\"faq-chip\" [class]=\"categoryClass(faq.category)\">{{ faq.category }}</span>\n } @else {\n <span class=\"eds-body-sm\">\u2014</span>\n }\n </td>\n <td>\n <label class=\"knowledge-hub__toggle\">\n <input\n type=\"checkbox\"\n [checked]=\"faq.isActive\"\n (change)=\"toggleActive(faq, $any($event.target).checked)\"\n [attr.aria-label]=\"'Toggle active for ' + faq.question\"\n />\n <span class=\"knowledge-hub__status\" [class.knowledge-hub__status--on]=\"faq.isActive\">\n {{ faq.isActive ? 'Active' : 'Inactive' }}\n </span>\n </label>\n </td>\n <td class=\"eds-mono-sm\">{{ faq.updatedAt | date: 'mediumDate' }}</td>\n <td class=\"knowledge-hub__actions\">\n <button type=\"button\" class=\"knowledge-hub__link\" (click)=\"openEdit(faq)\">Edit</button>\n <button type=\"button\" class=\"knowledge-hub__link knowledge-hub__link--danger\" (click)=\"deleteFaq(faq)\">\n Delete\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <footer class=\"knowledge-hub__pager\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"knowledge-hub__pager-actions\">\n <eds-button variant=\"secondary\" [disabled]=\"page() <= 1\" (click)=\"prevPage()\">Previous</eds-button>\n <eds-button variant=\"secondary\" [disabled]=\"page() >= totalPages()\" (click)=\"nextPage()\">Next</eds-button>\n </div>\n </footer>\n }\n </main>\n\n <eds-faq-search-tester />\n </div>\n\n <button type=\"button\" class=\"knowledge-hub__fab\" aria-label=\"Add FAQ\" (click)=\"openCreate()\">\n <eds-icon name=\"add\" [size]=\"24\" />\n </button>\n\n <eds-faq-form\n [open]=\"formOpen()\"\n [faq]=\"editing()\"\n [initialQuestion]=\"formSeed().question\"\n [initialAnswer]=\"formSeed().answer\"\n [initialCategory]=\"formSeed().category\"\n (closed)=\"closeForm()\"\n (saved)=\"onSave($event)\"\n />\n</div>\n", styles: [".faq-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize}.faq-chip--hours{background:#3b82f61f;color:#2563eb}.faq-chip--location{background:#14b8a61f;color:#0d9488}.faq-chip--parking{background:#6b72801f;color:#4b5563}.faq-chip--insurance{background:#8b5cf61f;color:#7c3aed}.faq-chip--services{background:#22c55e1f;color:#16a34a}.faq-chip--policies{background:#f973161f;color:#ea580c}.faq-chip--general{background:#6b72801f;color:#6b7280}.knowledge-hub__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}.knowledge-hub__banner{display:flex;align-items:flex-start;gap:10px;padding:12px 16px;margin-bottom:20px;background:#3b82f60f;border-color:#3b82f633}.knowledge-hub__banner p{margin:0}.knowledge-hub__layout{display:grid;grid-template-columns:1fr 320px;gap:20px;align-items:start}.knowledge-hub__tabs{display:flex;gap:4px;margin-bottom:12px;padding:4px;width:fit-content;background:var(--eds-surface-l1);border-radius:var(--eds-radius-md)}.knowledge-hub__tab{border:none;background:transparent;padding:8px 14px;border-radius:var(--eds-radius-sm);font-size:13px;cursor:pointer;color:var(--eds-text-secondary)}.knowledge-hub__tab--active{background:var(--eds-surface-l0);color:var(--eds-text-primary);font-weight:600;box-shadow:0 1px 2px #0000000f}.knowledge-hub__filters{padding:12px;margin-bottom:12px}.knowledge-hub__search{width:100%;padding:8px 12px;border:1px solid var(--eds-border-default);border-radius:var(--eds-radius-md);font-size:14px;margin-bottom:10px}.knowledge-hub__chips{display:flex;flex-wrap:wrap;gap:6px}.knowledge-hub__chip{border:1px solid transparent;cursor:pointer;opacity:.6}.knowledge-hub__chip--on{opacity:1;border-color:currentColor}.knowledge-hub__table-wrap{overflow-x:auto;padding:0}.knowledge-hub__question{font-weight:600;max-width:220px}.knowledge-hub__answer{color:var(--eds-text-secondary);max-width:260px}.knowledge-hub__row--inactive{opacity:.65}.knowledge-hub__toggle{display:flex;align-items:center;gap:6px;cursor:pointer}.knowledge-hub__status{font-size:12px;color:var(--eds-text-secondary)}.knowledge-hub__status--on{color:#16a34a}.knowledge-hub__status:before{content:\"\";display:inline-block;width:6px;height:6px;border-radius:50%;background:#9ca3af;margin-right:4px;vertical-align:middle}.knowledge-hub__status--on:before{background:#22c55e}.knowledge-hub__actions{white-space:nowrap;display:flex;gap:8px}.knowledge-hub__link{border:none;background:none;color:var(--eds-brand, #0d9488);font-size:13px;cursor:pointer;padding:0}.knowledge-hub__link--danger{color:#dc2626}.knowledge-hub__error{color:#b91c1c}.knowledge-hub__empty-filter{padding:24px;text-align:center;color:var(--eds-text-secondary)}.knowledge-hub__skeleton{padding:16px}.knowledge-hub__skel-row{height:48px;border-radius:8px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb,#f3f4f6 75%);background-size:200% 100%;animation:faq-shimmer 1.2s infinite;margin-bottom:8px}@keyframes faq-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.knowledge-hub__fab{display:none;position:fixed;right:24px;bottom:24px;width:56px;height:56px;border-radius:50%;border:none;background:var(--eds-brand, #0d9488);color:#fff;box-shadow:0 4px 14px #0003;cursor:pointer;align-items:center;justify-content:center}@media(max-width:960px){.knowledge-hub__layout{grid-template-columns:1fr}.knowledge-hub__add-desktop{display:none}.knowledge-hub__fab{display:inline-flex}}@media(min-width:961px){.knowledge-hub__fab{display:none}}.knowledge-hub__pager{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--eds-border-subtle, #e4e4e7)}.knowledge-hub__pager-actions{display:flex;gap:8px}\n"] }]
|
|
606
|
-
}] });
|
|
607
|
-
|
|
608
|
-
export { KnowledgeHubPageComponent };
|
|
609
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-knowledge-hub.page-BngGx_Lc.mjs.map
|