@edvak99/voice-receptionist-widget 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +56 -43
  2. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs +113 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs.map +1 -0
  4. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs +35 -0
  5. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs.map +1 -0
  6. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs +316 -0
  7. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs.map +1 -0
  8. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs +138 -0
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs.map +1 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs +110 -0
  11. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs.map +1 -0
  12. package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs +29 -0
  13. package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs.map +1 -0
  14. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +70 -0
  15. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +1 -0
  16. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs +248 -0
  17. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs.map +1 -0
  18. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +27 -0
  19. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +1 -0
  20. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs +136 -0
  21. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs.map +1 -0
  22. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs +2666 -0
  23. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs.map +1 -0
  24. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs +575 -0
  25. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs.map +1 -0
  26. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs +120 -0
  27. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs.map +1 -0
  28. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs +115 -0
  29. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs.map +1 -0
  30. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs +574 -0
  31. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs.map +1 -0
  32. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +54 -0
  33. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +1 -0
  34. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs +265 -0
  35. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs.map +1 -0
  36. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs +509 -0
  37. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs.map +1 -0
  38. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +52 -0
  39. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +1 -0
  40. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs +237 -0
  41. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs.map +1 -0
  42. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs +45 -0
  43. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs.map +1 -0
  44. package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -41
  45. package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
  46. package/lib/api-paths.d.ts +11 -0
  47. package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +45 -0
  48. package/lib/components/call-recording-player/call-recording-player.component.d.ts +29 -0
  49. package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +19 -0
  50. package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +36 -0
  51. package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +37 -0
  52. package/lib/components/outbound-purpose-form-modal/outbound-purpose-form-modal.component.d.ts +38 -0
  53. package/lib/components/task-detail-drawer/task-detail-drawer.component.d.ts +20 -0
  54. package/lib/config.d.ts +23 -0
  55. package/lib/extract-api-rows.d.ts +2 -0
  56. package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +22 -0
  57. package/lib/knowledge/components/faq-form/faq-form.component.d.ts +27 -0
  58. package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +17 -0
  59. package/lib/knowledge/mocks/practice-faq.seed.d.ts +2 -0
  60. package/lib/knowledge/models/practice-faq.model.d.ts +47 -0
  61. package/lib/knowledge/pages/knowledge-hub.page.d.ts +47 -0
  62. package/lib/knowledge/services/practice-faq-api.mock.d.ts +16 -0
  63. package/lib/knowledge/services/practice-faq-api.service.d.ts +22 -0
  64. package/lib/knowledge/utils/faq.util.d.ts +3 -0
  65. package/lib/layout/eds-app-rail.component.d.ts +6 -0
  66. package/lib/layout/eds-app-shell.component.d.ts +5 -0
  67. package/lib/layout/eds-module-nav.component.d.ts +24 -0
  68. package/lib/layout/eds-topbar.component.d.ts +29 -0
  69. package/lib/mocks/api-log.seed.d.ts +5 -0
  70. package/lib/mocks/outbound-purpose.seed.d.ts +3 -0
  71. package/lib/mocks/seed-data.d.ts +38 -0
  72. package/lib/models/ai-receptionist.types.d.ts +257 -0
  73. package/lib/models/api-dto.types.d.ts +215 -0
  74. package/lib/models/api-log.types.d.ts +53 -0
  75. package/lib/models/live-transcript.types.d.ts +26 -0
  76. package/lib/models/outbound-purpose.types.d.ts +50 -0
  77. package/lib/pages/analytics-page/analytics-page.component.d.ts +44 -0
  78. package/lib/pages/audit-page/audit-page.component.d.ts +14 -0
  79. package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +25 -0
  80. package/lib/pages/call-history-page/call-history-page.component.d.ts +42 -0
  81. package/lib/pages/dashboard-page/dashboard-page.component.d.ts +60 -0
  82. package/lib/pages/live-calls-page/live-calls-page.component.d.ts +37 -0
  83. package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +43 -0
  84. package/lib/pages/outbound-purposes-page/outbound-purposes-page.component.d.ts +24 -0
  85. package/lib/pages/settings-page/settings-fields.config.d.ts +35 -0
  86. package/lib/pages/settings-page/settings-page.component.d.ts +50 -0
  87. package/lib/pages/settings-page/tts-voice-presets.d.ts +7 -0
  88. package/lib/pages/tasks-page/tasks-page.component.d.ts +35 -0
  89. package/lib/pages/wizard-page/wizard-page.component.d.ts +17 -0
  90. package/lib/pipes/eds-elapsed.pipe.d.ts +7 -0
  91. package/lib/provide-voice-receptionist.d.ts +6 -0
  92. package/lib/routes.d.ts +8 -0
  93. package/lib/services/agent-online.service.d.ts +29 -0
  94. package/lib/services/ai-receptionist-api.mappers.d.ts +45 -0
  95. package/lib/services/ai-receptionist-api.mock.d.ts +150 -0
  96. package/lib/services/ai-receptionist-api.service.d.ts +156 -0
  97. package/lib/services/call-review.service.d.ts +12 -0
  98. package/lib/services/dashboard-socket.service.d.ts +26 -0
  99. package/lib/services/live-calls-list.service.d.ts +29 -0
  100. package/lib/services/live-transcript.service.d.ts +39 -0
  101. package/lib/shared/copy-button.component.d.ts +14 -0
  102. package/lib/shared/modal.component.d.ts +20 -0
  103. package/lib/shared/toast-container.component.d.ts +7 -0
  104. package/lib/shared/toast.service.d.ts +16 -0
  105. package/lib/ui/eds-avatar.component.d.ts +8 -0
  106. package/lib/ui/eds-badge.component.d.ts +7 -0
  107. package/lib/ui/eds-button.component.d.ts +9 -0
  108. package/lib/ui/eds-icon.component.d.ts +10 -0
  109. package/lib/ui/eds-pulse-dot.component.d.ts +5 -0
  110. package/lib/ui/eds-waveform.component.d.ts +8 -0
  111. package/lib/utils/call-outcome.util.d.ts +8 -0
  112. package/lib/utils/call-review.util.d.ts +8 -0
  113. package/lib/utils/outbound-purpose.util.d.ts +14 -0
  114. package/lib/utils/task.util.d.ts +10 -0
  115. package/lib/utils/tool-labels.util.d.ts +14 -0
  116. package/lib/utils/transcript-timing.util.d.ts +17 -0
  117. package/lib/utils/welcome-message.util.d.ts +5 -0
  118. package/package.json +12 -5
  119. package/public-api.d.ts +6 -2
  120. package/styles/_index.scss +55 -0
  121. package/styles/chat-ui.scss +133 -0
  122. package/styles/eds.scss +328 -0
  123. package/lib/voice-receptionist-widget.component.d.ts +0 -5
  124. package/lib/voice-receptionist-widget.service.d.ts +0 -6
@@ -0,0 +1,575 @@
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, E as EdsButtonComponent, M as ModalComponent, T as ToastService, b as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.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(activeOnly = false) {
201
+ if (this.useMock)
202
+ return this.mock.list(activeOnly);
203
+ let params = new HttpParams();
204
+ if (activeOnly)
205
+ params = params.set('activeOnly', 'true');
206
+ return this.http.get(this.base, { params }).pipe(map((body) => this.asList(body)));
207
+ }
208
+ get(faqId) {
209
+ if (this.useMock)
210
+ return this.mock.get(faqId);
211
+ const enc = encodeURIComponent(faqId);
212
+ return this.http.get(`${this.base}/${enc}`);
213
+ }
214
+ create(body) {
215
+ if (this.useMock)
216
+ return this.mock.create(body);
217
+ return this.http.post(this.base, body);
218
+ }
219
+ update(faqId, body) {
220
+ if (this.useMock)
221
+ return this.mock.update(faqId, body);
222
+ const enc = encodeURIComponent(faqId);
223
+ return this.http.patch(`${this.base}/${enc}`, body);
224
+ }
225
+ delete(faqId) {
226
+ if (this.useMock)
227
+ return this.mock.delete(faqId);
228
+ const enc = encodeURIComponent(faqId);
229
+ return this.http.delete(`${this.base}/${enc}`);
230
+ }
231
+ search(q, limit = 3) {
232
+ if (this.useMock)
233
+ return this.mock.search(q, limit);
234
+ const params = new HttpParams().set('q', q).set('limit', String(limit));
235
+ return this.http.get(`${this.base}/search`, { params });
236
+ }
237
+ asList(body) {
238
+ if (Array.isArray(body))
239
+ return body;
240
+ return body.items ?? [];
241
+ }
242
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
243
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, providedIn: 'root' }); }
244
+ }
245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PracticeFaqApiService, decorators: [{
246
+ type: Injectable,
247
+ args: [{ providedIn: 'root' }]
248
+ }] });
249
+
250
+ function categoryBadgeClass(category) {
251
+ if (!category)
252
+ return 'faq-chip--general';
253
+ const safe = category.toLowerCase().replace(/[^a-z0-9-]/g, '');
254
+ return `faq-chip--${safe || 'general'}`;
255
+ }
256
+ function truncateText(text, max = 80) {
257
+ const t = text?.trim() ?? '';
258
+ if (t.length <= max)
259
+ return t;
260
+ return `${t.slice(0, max)}…`;
261
+ }
262
+ function scorePercent(score) {
263
+ return Math.round(Math.min(1, Math.max(0, score)) * 100);
264
+ }
265
+
266
+ class FaqEmptyStateComponent {
267
+ constructor() {
268
+ this.starters = FAQ_STARTER_TEMPLATES;
269
+ this.add = new EventEmitter();
270
+ this.useTemplate = new EventEmitter();
271
+ }
272
+ onAdd() {
273
+ this.add.emit();
274
+ }
275
+ onTemplate(t) {
276
+ this.useTemplate.emit(t);
277
+ }
278
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqEmptyStateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
279
+ 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(--border-subtle, #d1d5db);background:var(--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 }); }
280
+ }
281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqEmptyStateComponent, decorators: [{
282
+ type: Component,
283
+ 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(--border-subtle, #d1d5db);background:var(--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"] }]
284
+ }], propDecorators: { add: [{
285
+ type: Output
286
+ }], useTemplate: [{
287
+ type: Output
288
+ }] } });
289
+
290
+ class FaqFormComponent {
291
+ constructor() {
292
+ this.open = false;
293
+ this.faq = null;
294
+ this.initialQuestion = '';
295
+ this.initialAnswer = '';
296
+ this.initialCategory = '';
297
+ this.closed = new EventEmitter();
298
+ this.saved = new EventEmitter();
299
+ this.categories = FAQ_CATEGORY_PRESETS;
300
+ this.saving = signal(false);
301
+ this.error = signal(null);
302
+ this.question = '';
303
+ this.answer = '';
304
+ this.category = '';
305
+ this.sortOrder = 0;
306
+ this.isActive = true;
307
+ }
308
+ get isEdit() {
309
+ return !!this.faq;
310
+ }
311
+ ngOnChanges() {
312
+ if (this.open)
313
+ this.resetForm();
314
+ }
315
+ close() {
316
+ this.closed.emit();
317
+ }
318
+ submit() {
319
+ this.error.set(null);
320
+ const q = this.question.trim();
321
+ const a = this.answer.trim();
322
+ if (q.length < 3 || q.length > 500) {
323
+ this.error.set('Question must be 3–500 characters.');
324
+ return;
325
+ }
326
+ if (a.length < 3 || a.length > 2000) {
327
+ this.error.set('Answer must be 3–2000 characters.');
328
+ return;
329
+ }
330
+ const cat = this.category.trim();
331
+ if (cat.length > 64) {
332
+ this.error.set('Category must be 64 characters or fewer.');
333
+ return;
334
+ }
335
+ const payload = {
336
+ question: q,
337
+ answer: a,
338
+ category: cat || undefined,
339
+ sortOrder: this.sortOrder,
340
+ isActive: this.isActive,
341
+ };
342
+ this.saved.emit(payload);
343
+ }
344
+ resetForm() {
345
+ this.error.set(null);
346
+ this.saving.set(false);
347
+ if (this.faq) {
348
+ this.question = this.faq.question;
349
+ this.answer = this.faq.answer;
350
+ this.category = this.faq.category ?? '';
351
+ this.sortOrder = this.faq.sortOrder;
352
+ this.isActive = this.faq.isActive;
353
+ }
354
+ else {
355
+ this.question = this.initialQuestion;
356
+ this.answer = this.initialAnswer;
357
+ this.category = this.initialCategory;
358
+ this.sortOrder = 0;
359
+ this.isActive = true;
360
+ }
361
+ }
362
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
363
+ 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(--text-secondary)}.faq-form__textarea,.faq-form__input{padding:8px 10px;border:1px solid var(--border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-form__textarea{resize:vertical}.faq-form__count{font-size:11px;color:var(--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(--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 }); }
364
+ }
365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqFormComponent, decorators: [{
366
+ type: Component,
367
+ 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(--text-secondary)}.faq-form__textarea,.faq-form__input{padding:8px 10px;border:1px solid var(--border-subtle, #e4e4e7);border-radius:8px;font:inherit}.faq-form__textarea{resize:vertical}.faq-form__count{font-size:11px;color:var(--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(--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"] }]
368
+ }], propDecorators: { open: [{
369
+ type: Input
370
+ }], faq: [{
371
+ type: Input
372
+ }], initialQuestion: [{
373
+ type: Input
374
+ }], initialAnswer: [{
375
+ type: Input
376
+ }], initialCategory: [{
377
+ type: Input
378
+ }], closed: [{
379
+ type: Output
380
+ }], saved: [{
381
+ type: Output
382
+ }] } });
383
+
384
+ class FaqSearchTesterComponent {
385
+ constructor() {
386
+ this.api = inject(PracticeFaqApiService);
387
+ this.query = '';
388
+ this.loading = signal(false);
389
+ this.searched = signal(false);
390
+ this.found = signal(false);
391
+ this.results = signal([]);
392
+ this.error = signal(null);
393
+ this.categoryClass = categoryBadgeClass;
394
+ this.scorePercent = scorePercent;
395
+ }
396
+ search() {
397
+ const q = this.query.trim();
398
+ if (!q)
399
+ return;
400
+ this.loading.set(true);
401
+ this.error.set(null);
402
+ this.searched.set(false);
403
+ this.api.search(q, 3).subscribe({
404
+ next: (res) => {
405
+ this.loading.set(false);
406
+ this.searched.set(true);
407
+ this.found.set(res.found);
408
+ this.results.set(res.items);
409
+ },
410
+ error: () => {
411
+ this.loading.set(false);
412
+ this.error.set('Search failed. Please try again.');
413
+ },
414
+ });
415
+ }
416
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqSearchTesterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
417
+ 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(--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(--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(--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(--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(--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 }); }
418
+ }
419
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FaqSearchTesterComponent, decorators: [{
420
+ type: Component,
421
+ 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(--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(--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(--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(--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(--text-secondary);margin:0;line-height:1.5}\n"] }]
422
+ }] });
423
+
424
+ class KnowledgeHubPageComponent {
425
+ constructor() {
426
+ this.api = inject(PracticeFaqApiService);
427
+ this.toast = inject(ToastService);
428
+ this.allFaqs = signal([]);
429
+ this.loading = signal(true);
430
+ this.error = signal(null);
431
+ this.activeFilter = signal('all');
432
+ this.categoryFilter = signal('');
433
+ this.listSearch = signal('');
434
+ this.formOpen = signal(false);
435
+ this.editing = signal(null);
436
+ this.formSeed = signal({
437
+ question: '',
438
+ answer: '',
439
+ category: '',
440
+ });
441
+ this.categories = FAQ_CATEGORY_PRESETS;
442
+ this.activeTabs = [
443
+ { id: 'all', label: 'All' },
444
+ { id: 'active', label: 'Active only' },
445
+ { id: 'inactive', label: 'Inactive' },
446
+ ];
447
+ this.faqs = computed(() => {
448
+ let rows = [...this.allFaqs()];
449
+ const active = this.activeFilter();
450
+ if (active === 'active')
451
+ rows = rows.filter((f) => f.isActive);
452
+ if (active === 'inactive')
453
+ rows = rows.filter((f) => !f.isActive);
454
+ const cat = this.categoryFilter();
455
+ if (cat)
456
+ rows = rows.filter((f) => (f.category ?? '') === cat);
457
+ const q = this.listSearch().trim().toLowerCase();
458
+ if (q) {
459
+ rows = rows.filter((f) => f.question.toLowerCase().includes(q) ||
460
+ f.answer.toLowerCase().includes(q) ||
461
+ (f.category?.toLowerCase().includes(q) ?? false));
462
+ }
463
+ return rows.sort((a, b) => a.sortOrder - b.sortOrder);
464
+ });
465
+ this.categoryClass = categoryBadgeClass;
466
+ this.truncate = truncateText;
467
+ }
468
+ ngOnInit() {
469
+ this.load();
470
+ }
471
+ load() {
472
+ this.loading.set(true);
473
+ this.error.set(null);
474
+ this.api.list().subscribe({
475
+ next: (items) => {
476
+ this.allFaqs.set(items);
477
+ this.loading.set(false);
478
+ },
479
+ error: () => {
480
+ this.loading.set(false);
481
+ this.error.set('Failed to load FAQs.');
482
+ },
483
+ });
484
+ }
485
+ setActiveFilter(id) {
486
+ this.activeFilter.set(id);
487
+ }
488
+ toggleCategory(cat) {
489
+ this.categoryFilter.update((c) => (c === cat ? '' : cat));
490
+ }
491
+ isCategorySelected(cat) {
492
+ return this.categoryFilter() === cat;
493
+ }
494
+ openCreate() {
495
+ this.editing.set(null);
496
+ this.formSeed.set({ question: '', answer: '', category: '' });
497
+ this.formOpen.set(true);
498
+ }
499
+ openEdit(faq) {
500
+ this.editing.set(faq);
501
+ this.formOpen.set(true);
502
+ }
503
+ closeForm() {
504
+ this.formOpen.set(false);
505
+ this.editing.set(null);
506
+ }
507
+ onStarterTemplate(t) {
508
+ this.editing.set(null);
509
+ this.formSeed.set({ question: t.question, answer: t.answer, category: t.category });
510
+ this.formOpen.set(true);
511
+ }
512
+ onSave(payload) {
513
+ const edit = this.editing();
514
+ if (edit) {
515
+ this.api.update(edit.faqId, payload).subscribe({
516
+ next: (updated) => {
517
+ this.allFaqs.update((list) => list.map((f) => (f.faqId === updated.faqId ? updated : f)));
518
+ this.closeForm();
519
+ this.toast.show('FAQ saved — voice agent updated', 'success');
520
+ },
521
+ error: () => this.toast.show('Failed to save FAQ', 'error'),
522
+ });
523
+ return;
524
+ }
525
+ this.api.create(payload).subscribe({
526
+ next: (created) => {
527
+ this.allFaqs.update((list) => [...list, created]);
528
+ this.closeForm();
529
+ this.toast.show('Your AI receptionist can now answer this on live calls.', 'success');
530
+ },
531
+ error: () => this.toast.show('Failed to create FAQ', 'error'),
532
+ });
533
+ }
534
+ toggleActive(faq, active) {
535
+ this.allFaqs.update((list) => list.map((f) => (f.faqId === faq.faqId ? { ...f, isActive: active } : f)));
536
+ this.api.update(faq.faqId, { isActive: active }).subscribe({
537
+ next: (updated) => {
538
+ this.allFaqs.update((list) => list.map((f) => (f.faqId === updated.faqId ? updated : f)));
539
+ },
540
+ error: () => {
541
+ this.allFaqs.update((list) => list.map((f) => (f.faqId === faq.faqId ? { ...f, isActive: !active } : f)));
542
+ this.toast.show('Failed to update FAQ status', 'error');
543
+ },
544
+ });
545
+ }
546
+ deleteFaq(faq) {
547
+ const msg = 'Remove this FAQ from the knowledge base? The voice agent will no longer use it.';
548
+ if (!confirm(msg))
549
+ return;
550
+ this.api.delete(faq.faqId).subscribe({
551
+ next: () => {
552
+ this.allFaqs.update((list) => list.filter((f) => f.faqId !== faq.faqId));
553
+ this.toast.show('FAQ removed', 'success');
554
+ },
555
+ error: () => this.toast.show('Failed to delete FAQ', 'error'),
556
+ });
557
+ }
558
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeHubPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
559
+ 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 (allFaqs().length === 0) {\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 </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(--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(--text-secondary)}.knowledge-hub__tab--active{background:var(--surface-l0);color:var(--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(--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(--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(--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(--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}}\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 }); }
560
+ }
561
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: KnowledgeHubPageComponent, decorators: [{
562
+ type: Component,
563
+ args: [{ selector: 'eds-knowledge-hub-page', imports: [
564
+ DatePipe,
565
+ FormsModule,
566
+ EdsButtonComponent,
567
+ EdsIconComponent,
568
+ FaqEmptyStateComponent,
569
+ FaqFormComponent,
570
+ FaqSearchTesterComponent,
571
+ ], 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 (allFaqs().length === 0) {\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 </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(--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(--text-secondary)}.knowledge-hub__tab--active{background:var(--surface-l0);color:var(--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(--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(--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(--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(--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}}\n"] }]
572
+ }] });
573
+
574
+ export { KnowledgeHubPageComponent };
575
+ //# sourceMappingURL=edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs.map