@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.
- package/README.md +56 -43
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs +113 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs +35 -0
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs +316 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-BV7KOhd9.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs +138 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CWWma-6r.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs +110 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-B2IuwsVH.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs +29 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-D-GutOar.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +70 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs +248 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B93giErR.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +27 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs +136 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BrMNsxB5.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs +2666 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs +575 -0
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-BNsM-o3z.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs +120 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-C2UZJ5wF.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs +115 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DQTVJjsP.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs +574 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-C3iTtVdn.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +54 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs +265 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-C10N-x5g.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs +509 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BBE_H2jb.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +52 -0
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs +237 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-COnYcaMT.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs +45 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-Box7X-Cq.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -41
- package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
- package/lib/api-paths.d.ts +11 -0
- package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +45 -0
- package/lib/components/call-recording-player/call-recording-player.component.d.ts +29 -0
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +19 -0
- package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +36 -0
- package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +37 -0
- package/lib/components/outbound-purpose-form-modal/outbound-purpose-form-modal.component.d.ts +38 -0
- package/lib/components/task-detail-drawer/task-detail-drawer.component.d.ts +20 -0
- package/lib/config.d.ts +23 -0
- package/lib/extract-api-rows.d.ts +2 -0
- package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +22 -0
- package/lib/knowledge/components/faq-form/faq-form.component.d.ts +27 -0
- package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +17 -0
- package/lib/knowledge/mocks/practice-faq.seed.d.ts +2 -0
- package/lib/knowledge/models/practice-faq.model.d.ts +47 -0
- package/lib/knowledge/pages/knowledge-hub.page.d.ts +47 -0
- package/lib/knowledge/services/practice-faq-api.mock.d.ts +16 -0
- package/lib/knowledge/services/practice-faq-api.service.d.ts +22 -0
- package/lib/knowledge/utils/faq.util.d.ts +3 -0
- package/lib/layout/eds-app-rail.component.d.ts +6 -0
- package/lib/layout/eds-app-shell.component.d.ts +5 -0
- package/lib/layout/eds-module-nav.component.d.ts +24 -0
- package/lib/layout/eds-topbar.component.d.ts +29 -0
- package/lib/mocks/api-log.seed.d.ts +5 -0
- package/lib/mocks/outbound-purpose.seed.d.ts +3 -0
- package/lib/mocks/seed-data.d.ts +38 -0
- package/lib/models/ai-receptionist.types.d.ts +257 -0
- package/lib/models/api-dto.types.d.ts +215 -0
- package/lib/models/api-log.types.d.ts +53 -0
- package/lib/models/live-transcript.types.d.ts +26 -0
- package/lib/models/outbound-purpose.types.d.ts +50 -0
- package/lib/pages/analytics-page/analytics-page.component.d.ts +44 -0
- package/lib/pages/audit-page/audit-page.component.d.ts +14 -0
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +25 -0
- package/lib/pages/call-history-page/call-history-page.component.d.ts +42 -0
- package/lib/pages/dashboard-page/dashboard-page.component.d.ts +60 -0
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +37 -0
- package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +43 -0
- package/lib/pages/outbound-purposes-page/outbound-purposes-page.component.d.ts +24 -0
- package/lib/pages/settings-page/settings-fields.config.d.ts +35 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +50 -0
- package/lib/pages/settings-page/tts-voice-presets.d.ts +7 -0
- package/lib/pages/tasks-page/tasks-page.component.d.ts +35 -0
- package/lib/pages/wizard-page/wizard-page.component.d.ts +17 -0
- package/lib/pipes/eds-elapsed.pipe.d.ts +7 -0
- package/lib/provide-voice-receptionist.d.ts +6 -0
- package/lib/routes.d.ts +8 -0
- package/lib/services/agent-online.service.d.ts +29 -0
- package/lib/services/ai-receptionist-api.mappers.d.ts +45 -0
- package/lib/services/ai-receptionist-api.mock.d.ts +150 -0
- package/lib/services/ai-receptionist-api.service.d.ts +156 -0
- package/lib/services/call-review.service.d.ts +12 -0
- package/lib/services/dashboard-socket.service.d.ts +26 -0
- package/lib/services/live-calls-list.service.d.ts +29 -0
- package/lib/services/live-transcript.service.d.ts +39 -0
- package/lib/shared/copy-button.component.d.ts +14 -0
- package/lib/shared/modal.component.d.ts +20 -0
- package/lib/shared/toast-container.component.d.ts +7 -0
- package/lib/shared/toast.service.d.ts +16 -0
- package/lib/ui/eds-avatar.component.d.ts +8 -0
- package/lib/ui/eds-badge.component.d.ts +7 -0
- package/lib/ui/eds-button.component.d.ts +9 -0
- package/lib/ui/eds-icon.component.d.ts +10 -0
- package/lib/ui/eds-pulse-dot.component.d.ts +5 -0
- package/lib/ui/eds-waveform.component.d.ts +8 -0
- package/lib/utils/call-outcome.util.d.ts +8 -0
- package/lib/utils/call-review.util.d.ts +8 -0
- package/lib/utils/outbound-purpose.util.d.ts +14 -0
- package/lib/utils/task.util.d.ts +10 -0
- package/lib/utils/tool-labels.util.d.ts +14 -0
- package/lib/utils/transcript-timing.util.d.ts +17 -0
- package/lib/utils/welcome-message.util.d.ts +5 -0
- package/package.json +12 -5
- package/public-api.d.ts +6 -2
- package/styles/_index.scss +55 -0
- package/styles/chat-ui.scss +133 -0
- package/styles/eds.scss +328 -0
- package/lib/voice-receptionist-widget.component.d.ts +0 -5
- package/lib/voice-receptionist-widget.service.d.ts +0 -6
package/README.md
CHANGED
|
@@ -1,63 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @edvak99/voice-receptionist-widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Embeddable **AI Receptionist** feature for Angular **19.2.x** hosts (EHR or standalone Voice Hub).
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
5
|
+
## Install
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
|
|
8
|
+
npm install @edvak99/voice-receptionist-widget socket.io-client
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
Peer deps: `@angular/core`, `common`, `forms`, `router` (^19.2), `rxjs`, `socket.io-client`.
|
|
12
|
+
|
|
13
|
+
## Wire into EHR
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
provideVoiceReceptionist,
|
|
18
|
+
VOICE_RECEPTIONIST_ROUTES,
|
|
19
|
+
} from '@edvak99/voice-receptionist-widget';
|
|
20
|
+
|
|
21
|
+
export const appConfig = {
|
|
22
|
+
providers: [
|
|
23
|
+
provideHttpClient(withInterceptors([/* your auth interceptor */])),
|
|
24
|
+
provideVoiceReceptionist({
|
|
25
|
+
apiBaseUrl: 'https://your-brain-api.example.com',
|
|
26
|
+
// includePracticeNav: false // default — hide patients/providers in embed
|
|
27
|
+
}),
|
|
28
|
+
provideRouter([
|
|
29
|
+
{
|
|
30
|
+
path: 'ai-receptionist',
|
|
31
|
+
children: VOICE_RECEPTIONIST_ROUTES,
|
|
32
|
+
},
|
|
33
|
+
]),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
17
36
|
```
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
To build the library, run:
|
|
38
|
+
Or lazy:
|
|
22
39
|
|
|
23
|
-
```
|
|
24
|
-
|
|
40
|
+
```ts
|
|
41
|
+
{
|
|
42
|
+
path: 'ai-receptionist',
|
|
43
|
+
loadChildren: () =>
|
|
44
|
+
import('@edvak99/voice-receptionist-widget').then((m) => m.VOICE_RECEPTIONIST_ROUTES),
|
|
45
|
+
}
|
|
25
46
|
```
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npm publish
|
|
41
|
-
```
|
|
48
|
+
Host owns login and attaches `Authorization` via its HTTP interceptor. Optional session for topbar:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
provideVoiceReceptionist({
|
|
52
|
+
apiBaseUrl: '...',
|
|
53
|
+
session: {
|
|
54
|
+
email: computed(() => auth.session()?.email ?? null),
|
|
55
|
+
isAdmin: () => auth.isAdmin(),
|
|
56
|
+
logout: () => auth.logout(),
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
```
|
|
42
60
|
|
|
43
|
-
##
|
|
61
|
+
## Styles & fonts
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
In the host global stylesheet:
|
|
46
64
|
|
|
47
|
-
```
|
|
48
|
-
|
|
65
|
+
```scss
|
|
66
|
+
@use '@edvak99/voice-receptionist-widget/styles';
|
|
49
67
|
```
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
Also load Material Symbols + fonts (Inter, DM Sans, Figtree, IBM Plex Mono) in `index.html`, same as the Voice Hub app.
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
## Build this package
|
|
54
72
|
|
|
55
73
|
```bash
|
|
56
|
-
ng
|
|
74
|
+
npx ng build voice-receptionist-widget
|
|
75
|
+
cd dist/voice-receptionist-widget && npm publish --access public
|
|
57
76
|
```
|
|
58
|
-
|
|
59
|
-
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
|
60
|
-
|
|
61
|
-
## Additional Resources
|
|
62
|
-
|
|
63
|
-
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { A as AiReceptionistApiService, b as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs';
|
|
4
|
+
|
|
5
|
+
class AnalyticsPageComponent {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.api = inject(AiReceptionistApiService);
|
|
8
|
+
this.data = signal(null);
|
|
9
|
+
this.range = signal('30d');
|
|
10
|
+
this.loading = signal(true);
|
|
11
|
+
this.ranges = [
|
|
12
|
+
{ id: '7d', label: '7 days' },
|
|
13
|
+
{ id: '30d', label: '30 days' },
|
|
14
|
+
{ id: '90d', label: '90 days' },
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
ngOnInit() {
|
|
18
|
+
this.load();
|
|
19
|
+
}
|
|
20
|
+
setRange(id) {
|
|
21
|
+
this.range.set(id);
|
|
22
|
+
this.load();
|
|
23
|
+
}
|
|
24
|
+
maxCount(items) {
|
|
25
|
+
return Math.max(...items.map((i) => i.count), 1);
|
|
26
|
+
}
|
|
27
|
+
barHeightPercent(point, points) {
|
|
28
|
+
if (point.count <= 0)
|
|
29
|
+
return 0;
|
|
30
|
+
return (point.count / this.maxCount(points)) * 100;
|
|
31
|
+
}
|
|
32
|
+
/** Hide inline counts when bars are too narrow to stay readable. */
|
|
33
|
+
showBarValue(point, points) {
|
|
34
|
+
return point.count > 0 && points.length <= 21;
|
|
35
|
+
}
|
|
36
|
+
maxValue(items) {
|
|
37
|
+
return Math.max(...items.map((i) => i.value), 1);
|
|
38
|
+
}
|
|
39
|
+
volumeTooltip(point) {
|
|
40
|
+
const datePart = point.date ? this.formatChartDate(point.date) : point.label;
|
|
41
|
+
return `${datePart}: ${point.count} call${point.count === 1 ? '' : 's'}`;
|
|
42
|
+
}
|
|
43
|
+
peakTooltip(point) {
|
|
44
|
+
return `${point.label}: ${point.count} call${point.count === 1 ? '' : 's'}`;
|
|
45
|
+
}
|
|
46
|
+
/** ISO or YYYY-MM-DD → MM/DD (zero-padded). */
|
|
47
|
+
formatChartDate(date) {
|
|
48
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(date.trim());
|
|
49
|
+
if (iso) {
|
|
50
|
+
return `${iso[2]}/${iso[3]}`;
|
|
51
|
+
}
|
|
52
|
+
return date;
|
|
53
|
+
}
|
|
54
|
+
/** Compact x-axis for volume — full labels on 7d, sparse dates on longer ranges. */
|
|
55
|
+
volumeAxisLabel(point, index, total) {
|
|
56
|
+
if (point.date) {
|
|
57
|
+
if (total <= 7)
|
|
58
|
+
return this.formatChartDate(point.date);
|
|
59
|
+
const step = total <= 14 ? 2 : total <= 31 ? 5 : 7;
|
|
60
|
+
if (index % step !== 0 && index !== total - 1)
|
|
61
|
+
return '';
|
|
62
|
+
return this.formatChartDate(point.date);
|
|
63
|
+
}
|
|
64
|
+
if (total <= 7)
|
|
65
|
+
return point.day;
|
|
66
|
+
const step = total <= 14 ? 2 : total <= 31 ? 5 : 7;
|
|
67
|
+
if (index % step !== 0 && index !== total - 1)
|
|
68
|
+
return '';
|
|
69
|
+
return point.day;
|
|
70
|
+
}
|
|
71
|
+
showPeakAxisLabel(index, total) {
|
|
72
|
+
if (total <= 12)
|
|
73
|
+
return true;
|
|
74
|
+
const step = total <= 24 ? 3 : 4;
|
|
75
|
+
return index % step === 0 || index === total - 1;
|
|
76
|
+
}
|
|
77
|
+
volumeRangeLabel() {
|
|
78
|
+
const vol = this.data()?.volume;
|
|
79
|
+
if (!vol?.startDate || !vol?.endDate)
|
|
80
|
+
return 'Calls per day';
|
|
81
|
+
const start = this.formatChartDate(vol.startDate);
|
|
82
|
+
const end = this.formatChartDate(vol.endDate);
|
|
83
|
+
return `${start} – ${end}${vol.timezone ? ` (${vol.timezone})` : ''}`;
|
|
84
|
+
}
|
|
85
|
+
kpiIcon(label) {
|
|
86
|
+
const key = label.toLowerCase();
|
|
87
|
+
if (key.includes('call') || key.includes('handled'))
|
|
88
|
+
return 'call';
|
|
89
|
+
if (key.includes('time') || key.includes('duration'))
|
|
90
|
+
return 'schedule';
|
|
91
|
+
if (key.includes('resolution') || key.includes('book'))
|
|
92
|
+
return 'event_available';
|
|
93
|
+
if (key.includes('transfer'))
|
|
94
|
+
return 'phone_forwarded';
|
|
95
|
+
return 'monitoring';
|
|
96
|
+
}
|
|
97
|
+
load() {
|
|
98
|
+
this.loading.set(true);
|
|
99
|
+
this.api.getAnalytics(this.range()).subscribe(({ data }) => {
|
|
100
|
+
this.data.set(data);
|
|
101
|
+
this.loading.set(false);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AnalyticsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
105
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: AnalyticsPageComponent, isStandalone: true, selector: "eds-analytics-page", ngImport: i0, template: "<div class=\"eds-page analytics-page\">\n <header class=\"analytics-page__header\">\n <div>\n <h1 class=\"eds-h1\">Analytics</h1>\n <p class=\"eds-body-sm\">Call volume, outcomes, and performance KPIs</p>\n </div>\n <div class=\"analytics-page__range\">\n @for (r of ranges; track r.id) {\n <button\n type=\"button\"\n class=\"analytics-page__range-btn\"\n [class.analytics-page__range-btn--active]=\"range() === r.id\"\n (click)=\"setRange(r.id)\"\n >\n {{ r.label }}\n </button>\n }\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading analytics\u2026</p>\n } @else if (data()) {\n <section class=\"analytics-page__kpis\">\n @for (kpi of data()!.kpiCards; track kpi.label) {\n <article class=\"eds-card analytics-page__kpi\">\n <eds-icon [name]=\"kpiIcon(kpi.label)\" [size]=\"22\" />\n <span class=\"eds-label\">{{ kpi.label }}</span>\n <span class=\"analytics-page__kpi-value\">{{ kpi.value }}</span>\n @if (kpi.delta) {\n <span class=\"analytics-page__kpi-delta\" [attr.data-tone]=\"kpi.tone\">{{ kpi.delta }}</span>\n }\n @if (kpi.sub) {\n <span class=\"analytics-page__kpi-sub\">{{ kpi.sub }}</span>\n }\n </article>\n }\n </section>\n\n <div class=\"analytics-page__charts\">\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Call volume</h2>\n <span class=\"eds-label\">{{ volumeRangeLabel() }}</span>\n </div>\n <div class=\"analytics-page__volume-chart\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars\" role=\"list\" aria-label=\"Call volume by day\">\n @for (point of data()!.volume.points; track point.date || point.label; let i = $index) {\n <div\n class=\"analytics-page__bar-col\"\n role=\"listitem\"\n [attr.aria-label]=\"volumeTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.volume.points)\"\n >\n @if (showBarValue(point, data()!.volume.points)) {\n <span class=\"analytics-page__bar-value eds-mono-sm\">{{ point.count }}</span>\n }\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ volumeTooltip(point) }}</span>\n </div>\n <span class=\"eds-mono-sm analytics-page__bar-axis\">\n {{ volumeAxisLabel(point, i, data()!.volume.points.length) }}\n </span>\n </div>\n }\n </div>\n </div>\n </div>\n <!-- <p class=\"analytics-page__axis-note eds-label\">(MM/DD)/calls per day</p> -->\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Outcomes</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.outcomes; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.outcomes)) * 100\"\n [style.background]=\"item.color\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Top reasons</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.reasons; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.reasons)) * 100\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Peak hours</h2>\n <span class=\"eds-label\">\n @if (data()!.peakHours.timezone) {\n {{ data()!.peakHours.timezone }}\n } @else {\n Calls per hour\n }\n </span>\n </div>\n <div class=\"analytics-page__volume-chart analytics-page__volume-chart--peak\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars analytics-page__bars--peak\" role=\"list\" aria-label=\"Call volume by hour\">\n @for (point of data()!.peakHours.points; track point.hour) {\n <div\n class=\"analytics-page__bar-col analytics-page__bar-col--peak\"\n role=\"listitem\"\n [attr.aria-label]=\"peakTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.peakHours.points)\"\n >\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ peakTooltip(point) }}</span>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n <div class=\"analytics-page__peak-axis\">\n @for (point of data()!.peakHours.points; track point.hour; let i = $index) {\n <span class=\"analytics-page__peak-tick eds-mono-sm\">\n @if (showPeakAxisLabel(i, data()!.peakHours.points.length)) {\n {{ point.label }}\n }\n </span>\n }\n </div>\n </article>\n </div>\n }\n</div>\n", styles: [".analytics-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}.analytics-page__range{display:flex;gap:4px;background:var(--surface-l0);border:1px solid var(--border-default);border-radius:var(--eds-radius-md);padding:4px}.analytics-page__range-btn{padding:6px 12px;border:none;background:none;font-size:13px;border-radius:var(--eds-radius-sm);cursor:pointer;color:var(--text-secondary)}.analytics-page__range-btn--active{background:var(--brand-500);color:#fff}.analytics-page__kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}.analytics-page__kpi{display:flex;flex-direction:column;gap:6px;padding:16px}.analytics-page__kpi-value{font-size:24px;font-weight:600;color:var(--text-primary)}.analytics-page__kpi-delta{font-size:12px;font-weight:600}.analytics-page__kpi-delta[data-tone=pos]{color:var(--green-500, #16a34a)}.analytics-page__kpi-delta[data-tone=warn]{color:var(--amber-500, #d97706)}.analytics-page__kpi-delta[data-tone=neutral]{color:var(--text-secondary)}.analytics-page__kpi-sub{font-size:12px;color:var(--text-secondary)}.analytics-page__charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}.analytics-page__chart{padding:16px}.analytics-page__chart .eds-h2{margin-bottom:0}.analytics-page__chart-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px;flex-wrap:wrap}.analytics-page__volume-chart--peak .analytics-page__grid{inset:0}.analytics-page__plot{position:relative;width:100%;min-width:0}.analytics-page__grid{position:absolute;inset:0 0 24px;pointer-events:none;background-image:linear-gradient(to top,var(--border-default) 1px,transparent 1px);background-size:100% 25%;opacity:.45}.analytics-page__bars{display:flex;align-items:flex-end;justify-content:space-between;gap:8px;height:160px;position:relative;z-index:1}.analytics-page__bars--horizontal{flex-direction:column;height:auto;align-items:stretch;gap:10px}.analytics-page__bars--peak{height:100px}.analytics-page__bars--peak .analytics-page__bar{max-width:100%}.analytics-page__bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;height:100%;min-width:0;cursor:default}.analytics-page__bar-col:hover .analytics-page__tooltip,.analytics-page__bar-col:focus-within .analytics-page__tooltip{opacity:1}.analytics-page__bar-col--peak{min-width:0}.analytics-page__bar-value{flex-shrink:0;color:var(--text-primary);font-size:11px;font-weight:600;line-height:1}.analytics-page__bar-wrap{position:relative;width:100%;flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;min-height:0}.analytics-page__bar-stack{width:100%;max-width:32px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch;gap:4px;min-height:0}.analytics-page__bar-axis{flex-shrink:0;min-height:16px;color:var(--text-secondary);font-size:10px;text-align:center;line-height:1.2}.analytics-page__tooltip{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);padding:4px 8px;background:var(--gray-900, #18181b);color:#fafafa;font-size:11px;font-weight:500;line-height:1.3;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s ease;z-index:2;box-shadow:0 2px 8px #00000026}.analytics-page__tooltip:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);border:4px solid transparent;border-top-color:var(--gray-900, #18181b)}.analytics-page__peak-axis{display:flex;justify-content:space-between;gap:2px;margin-top:8px}.analytics-page__peak-tick{flex:1;text-align:center;min-height:16px;color:var(--text-secondary);font-size:10px}.analytics-page__bar{width:100%;flex:1 1 auto;min-height:3px;background:linear-gradient(180deg,var(--brand-500),var(--violet-500));border-radius:4px 4px 0 0;animation:eds-bar-grow .5s ease forwards;transform-origin:bottom}.analytics-page__hbar-row{display:grid;grid-template-columns:120px 1fr 40px;align-items:center;gap:10px}.analytics-page__hbar-track{height:8px;background:var(--surface-l2);border-radius:4px;overflow:hidden}.analytics-page__hbar-fill{display:block;height:100%;background:var(--brand-500);border-radius:4px;animation:eds-bar-grow .5s ease forwards;transform-origin:left}@media(max-width:1000px){.analytics-page__kpis{grid-template-columns:repeat(2,1fr)}.analytics-page__charts{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
106
|
+
}
|
|
107
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AnalyticsPageComponent, decorators: [{
|
|
108
|
+
type: Component,
|
|
109
|
+
args: [{ selector: 'eds-analytics-page', imports: [EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page analytics-page\">\n <header class=\"analytics-page__header\">\n <div>\n <h1 class=\"eds-h1\">Analytics</h1>\n <p class=\"eds-body-sm\">Call volume, outcomes, and performance KPIs</p>\n </div>\n <div class=\"analytics-page__range\">\n @for (r of ranges; track r.id) {\n <button\n type=\"button\"\n class=\"analytics-page__range-btn\"\n [class.analytics-page__range-btn--active]=\"range() === r.id\"\n (click)=\"setRange(r.id)\"\n >\n {{ r.label }}\n </button>\n }\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading analytics\u2026</p>\n } @else if (data()) {\n <section class=\"analytics-page__kpis\">\n @for (kpi of data()!.kpiCards; track kpi.label) {\n <article class=\"eds-card analytics-page__kpi\">\n <eds-icon [name]=\"kpiIcon(kpi.label)\" [size]=\"22\" />\n <span class=\"eds-label\">{{ kpi.label }}</span>\n <span class=\"analytics-page__kpi-value\">{{ kpi.value }}</span>\n @if (kpi.delta) {\n <span class=\"analytics-page__kpi-delta\" [attr.data-tone]=\"kpi.tone\">{{ kpi.delta }}</span>\n }\n @if (kpi.sub) {\n <span class=\"analytics-page__kpi-sub\">{{ kpi.sub }}</span>\n }\n </article>\n }\n </section>\n\n <div class=\"analytics-page__charts\">\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Call volume</h2>\n <span class=\"eds-label\">{{ volumeRangeLabel() }}</span>\n </div>\n <div class=\"analytics-page__volume-chart\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars\" role=\"list\" aria-label=\"Call volume by day\">\n @for (point of data()!.volume.points; track point.date || point.label; let i = $index) {\n <div\n class=\"analytics-page__bar-col\"\n role=\"listitem\"\n [attr.aria-label]=\"volumeTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.volume.points)\"\n >\n @if (showBarValue(point, data()!.volume.points)) {\n <span class=\"analytics-page__bar-value eds-mono-sm\">{{ point.count }}</span>\n }\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ volumeTooltip(point) }}</span>\n </div>\n <span class=\"eds-mono-sm analytics-page__bar-axis\">\n {{ volumeAxisLabel(point, i, data()!.volume.points.length) }}\n </span>\n </div>\n }\n </div>\n </div>\n </div>\n <!-- <p class=\"analytics-page__axis-note eds-label\">(MM/DD)/calls per day</p> -->\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Outcomes</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.outcomes; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.outcomes)) * 100\"\n [style.background]=\"item.color\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Top reasons</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.reasons; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.reasons)) * 100\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Peak hours</h2>\n <span class=\"eds-label\">\n @if (data()!.peakHours.timezone) {\n {{ data()!.peakHours.timezone }}\n } @else {\n Calls per hour\n }\n </span>\n </div>\n <div class=\"analytics-page__volume-chart analytics-page__volume-chart--peak\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars analytics-page__bars--peak\" role=\"list\" aria-label=\"Call volume by hour\">\n @for (point of data()!.peakHours.points; track point.hour) {\n <div\n class=\"analytics-page__bar-col analytics-page__bar-col--peak\"\n role=\"listitem\"\n [attr.aria-label]=\"peakTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.peakHours.points)\"\n >\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ peakTooltip(point) }}</span>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n <div class=\"analytics-page__peak-axis\">\n @for (point of data()!.peakHours.points; track point.hour; let i = $index) {\n <span class=\"analytics-page__peak-tick eds-mono-sm\">\n @if (showPeakAxisLabel(i, data()!.peakHours.points.length)) {\n {{ point.label }}\n }\n </span>\n }\n </div>\n </article>\n </div>\n }\n</div>\n", styles: [".analytics-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}.analytics-page__range{display:flex;gap:4px;background:var(--surface-l0);border:1px solid var(--border-default);border-radius:var(--eds-radius-md);padding:4px}.analytics-page__range-btn{padding:6px 12px;border:none;background:none;font-size:13px;border-radius:var(--eds-radius-sm);cursor:pointer;color:var(--text-secondary)}.analytics-page__range-btn--active{background:var(--brand-500);color:#fff}.analytics-page__kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}.analytics-page__kpi{display:flex;flex-direction:column;gap:6px;padding:16px}.analytics-page__kpi-value{font-size:24px;font-weight:600;color:var(--text-primary)}.analytics-page__kpi-delta{font-size:12px;font-weight:600}.analytics-page__kpi-delta[data-tone=pos]{color:var(--green-500, #16a34a)}.analytics-page__kpi-delta[data-tone=warn]{color:var(--amber-500, #d97706)}.analytics-page__kpi-delta[data-tone=neutral]{color:var(--text-secondary)}.analytics-page__kpi-sub{font-size:12px;color:var(--text-secondary)}.analytics-page__charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}.analytics-page__chart{padding:16px}.analytics-page__chart .eds-h2{margin-bottom:0}.analytics-page__chart-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px;flex-wrap:wrap}.analytics-page__volume-chart--peak .analytics-page__grid{inset:0}.analytics-page__plot{position:relative;width:100%;min-width:0}.analytics-page__grid{position:absolute;inset:0 0 24px;pointer-events:none;background-image:linear-gradient(to top,var(--border-default) 1px,transparent 1px);background-size:100% 25%;opacity:.45}.analytics-page__bars{display:flex;align-items:flex-end;justify-content:space-between;gap:8px;height:160px;position:relative;z-index:1}.analytics-page__bars--horizontal{flex-direction:column;height:auto;align-items:stretch;gap:10px}.analytics-page__bars--peak{height:100px}.analytics-page__bars--peak .analytics-page__bar{max-width:100%}.analytics-page__bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;height:100%;min-width:0;cursor:default}.analytics-page__bar-col:hover .analytics-page__tooltip,.analytics-page__bar-col:focus-within .analytics-page__tooltip{opacity:1}.analytics-page__bar-col--peak{min-width:0}.analytics-page__bar-value{flex-shrink:0;color:var(--text-primary);font-size:11px;font-weight:600;line-height:1}.analytics-page__bar-wrap{position:relative;width:100%;flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;min-height:0}.analytics-page__bar-stack{width:100%;max-width:32px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch;gap:4px;min-height:0}.analytics-page__bar-axis{flex-shrink:0;min-height:16px;color:var(--text-secondary);font-size:10px;text-align:center;line-height:1.2}.analytics-page__tooltip{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);padding:4px 8px;background:var(--gray-900, #18181b);color:#fafafa;font-size:11px;font-weight:500;line-height:1.3;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s ease;z-index:2;box-shadow:0 2px 8px #00000026}.analytics-page__tooltip:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);border:4px solid transparent;border-top-color:var(--gray-900, #18181b)}.analytics-page__peak-axis{display:flex;justify-content:space-between;gap:2px;margin-top:8px}.analytics-page__peak-tick{flex:1;text-align:center;min-height:16px;color:var(--text-secondary);font-size:10px}.analytics-page__bar{width:100%;flex:1 1 auto;min-height:3px;background:linear-gradient(180deg,var(--brand-500),var(--violet-500));border-radius:4px 4px 0 0;animation:eds-bar-grow .5s ease forwards;transform-origin:bottom}.analytics-page__hbar-row{display:grid;grid-template-columns:120px 1fr 40px;align-items:center;gap:10px}.analytics-page__hbar-track{height:8px;background:var(--surface-l2);border-radius:4px;overflow:hidden}.analytics-page__hbar-fill{display:block;height:100%;background:var(--brand-500);border-radius:4px;animation:eds-bar-grow .5s ease forwards;transform-origin:left}@media(max-width:1000px){.analytics-page__kpis{grid-template-columns:repeat(2,1fr)}.analytics-page__charts{grid-template-columns:1fr}}\n"] }]
|
|
110
|
+
}] });
|
|
111
|
+
|
|
112
|
+
export { AnalyticsPageComponent };
|
|
113
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edvak99-voice-receptionist-widget-analytics-page.component-CoVjr5Y3.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/pages/analytics-page/analytics-page.component.ts","../../../projects/voice-receptionist-widget/src/lib/pages/analytics-page/analytics-page.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';\n\nimport {\n AnalyticsData,\n AnalyticsPeakPoint,\n AnalyticsVolumePoint,\n} from '../../models/ai-receptionist.types';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\n\n@Component({\n selector: 'eds-analytics-page',\n imports: [EdsIconComponent],\n templateUrl: './analytics-page.component.html',\n styleUrl: './analytics-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class AnalyticsPageComponent implements OnInit {\n private readonly api = inject(AiReceptionistApiService);\n\n protected readonly data = signal<AnalyticsData | null>(null);\n protected readonly range = signal('30d');\n protected readonly loading = signal(true);\n\n protected readonly ranges = [\n { id: '7d', label: '7 days' },\n { id: '30d', label: '30 days' },\n { id: '90d', label: '90 days' },\n ];\n\n ngOnInit(): void {\n this.load();\n }\n\n protected setRange(id: string): void {\n this.range.set(id);\n this.load();\n }\n\n protected maxCount(items: { count: number }[]): number {\n return Math.max(...items.map((i) => i.count), 1);\n }\n\n protected barHeightPercent(point: { count: number }, points: { count: number }[]): number {\n if (point.count <= 0) return 0;\n return (point.count / this.maxCount(points)) * 100;\n }\n\n /** Hide inline counts when bars are too narrow to stay readable. */\n protected showBarValue(point: { count: number }, points: { count: number }[]): boolean {\n return point.count > 0 && points.length <= 21;\n }\n\n protected maxValue(items: { value: number }[]): number {\n return Math.max(...items.map((i) => i.value), 1);\n }\n\n protected volumeTooltip(point: AnalyticsVolumePoint): string {\n const datePart = point.date ? this.formatChartDate(point.date) : point.label;\n return `${datePart}: ${point.count} call${point.count === 1 ? '' : 's'}`;\n }\n\n protected peakTooltip(point: AnalyticsPeakPoint): string {\n return `${point.label}: ${point.count} call${point.count === 1 ? '' : 's'}`;\n }\n\n /** ISO or YYYY-MM-DD → MM/DD (zero-padded). */\n protected formatChartDate(date: string): string {\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(date.trim());\n if (iso) {\n return `${iso[2]}/${iso[3]}`;\n }\n return date;\n }\n\n /** Compact x-axis for volume — full labels on 7d, sparse dates on longer ranges. */\n protected volumeAxisLabel(point: AnalyticsVolumePoint, index: number, total: number): string {\n if (point.date) {\n if (total <= 7) return this.formatChartDate(point.date);\n const step = total <= 14 ? 2 : total <= 31 ? 5 : 7;\n if (index % step !== 0 && index !== total - 1) return '';\n return this.formatChartDate(point.date);\n }\n if (total <= 7) return point.day;\n const step = total <= 14 ? 2 : total <= 31 ? 5 : 7;\n if (index % step !== 0 && index !== total - 1) return '';\n return point.day;\n }\n\n protected showPeakAxisLabel(index: number, total: number): boolean {\n if (total <= 12) return true;\n const step = total <= 24 ? 3 : 4;\n return index % step === 0 || index === total - 1;\n }\n\n protected volumeRangeLabel(): string {\n const vol = this.data()?.volume;\n if (!vol?.startDate || !vol?.endDate) return 'Calls per day';\n const start = this.formatChartDate(vol.startDate);\n const end = this.formatChartDate(vol.endDate);\n return `${start} – ${end}${vol.timezone ? ` (${vol.timezone})` : ''}`;\n }\n\n protected kpiIcon(label: string): string {\n const key = label.toLowerCase();\n if (key.includes('call') || key.includes('handled')) return 'call';\n if (key.includes('time') || key.includes('duration')) return 'schedule';\n if (key.includes('resolution') || key.includes('book')) return 'event_available';\n if (key.includes('transfer')) return 'phone_forwarded';\n return 'monitoring';\n }\n\n private load(): void {\n this.loading.set(true);\n this.api.getAnalytics(this.range()).subscribe(({ data }) => {\n this.data.set(data);\n this.loading.set(false);\n });\n }\n}\n","<div class=\"eds-page analytics-page\">\n <header class=\"analytics-page__header\">\n <div>\n <h1 class=\"eds-h1\">Analytics</h1>\n <p class=\"eds-body-sm\">Call volume, outcomes, and performance KPIs</p>\n </div>\n <div class=\"analytics-page__range\">\n @for (r of ranges; track r.id) {\n <button\n type=\"button\"\n class=\"analytics-page__range-btn\"\n [class.analytics-page__range-btn--active]=\"range() === r.id\"\n (click)=\"setRange(r.id)\"\n >\n {{ r.label }}\n </button>\n }\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading analytics…</p>\n } @else if (data()) {\n <section class=\"analytics-page__kpis\">\n @for (kpi of data()!.kpiCards; track kpi.label) {\n <article class=\"eds-card analytics-page__kpi\">\n <eds-icon [name]=\"kpiIcon(kpi.label)\" [size]=\"22\" />\n <span class=\"eds-label\">{{ kpi.label }}</span>\n <span class=\"analytics-page__kpi-value\">{{ kpi.value }}</span>\n @if (kpi.delta) {\n <span class=\"analytics-page__kpi-delta\" [attr.data-tone]=\"kpi.tone\">{{ kpi.delta }}</span>\n }\n @if (kpi.sub) {\n <span class=\"analytics-page__kpi-sub\">{{ kpi.sub }}</span>\n }\n </article>\n }\n </section>\n\n <div class=\"analytics-page__charts\">\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Call volume</h2>\n <span class=\"eds-label\">{{ volumeRangeLabel() }}</span>\n </div>\n <div class=\"analytics-page__volume-chart\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars\" role=\"list\" aria-label=\"Call volume by day\">\n @for (point of data()!.volume.points; track point.date || point.label; let i = $index) {\n <div\n class=\"analytics-page__bar-col\"\n role=\"listitem\"\n [attr.aria-label]=\"volumeTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.volume.points)\"\n >\n @if (showBarValue(point, data()!.volume.points)) {\n <span class=\"analytics-page__bar-value eds-mono-sm\">{{ point.count }}</span>\n }\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ volumeTooltip(point) }}</span>\n </div>\n <span class=\"eds-mono-sm analytics-page__bar-axis\">\n {{ volumeAxisLabel(point, i, data()!.volume.points.length) }}\n </span>\n </div>\n }\n </div>\n </div>\n </div>\n <!-- <p class=\"analytics-page__axis-note eds-label\">(MM/DD)/calls per day</p> -->\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Outcomes</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.outcomes; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.outcomes)) * 100\"\n [style.background]=\"item.color\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <h2 class=\"eds-h2\">Top reasons</h2>\n <div class=\"analytics-page__bars analytics-page__bars--horizontal\">\n @for (item of data()!.reasons; track item.label) {\n <div class=\"analytics-page__hbar-row\">\n <span class=\"eds-body-sm\">{{ item.label }}</span>\n <div class=\"analytics-page__hbar-track\">\n <span\n class=\"analytics-page__hbar-fill\"\n [style.width.%]=\"(item.value / maxValue(data()!.reasons)) * 100\"\n ></span>\n </div>\n <span class=\"eds-mono-sm\">{{ item.value }}%</span>\n </div>\n }\n </div>\n </article>\n\n <article class=\"eds-card analytics-page__chart\">\n <div class=\"analytics-page__chart-head\">\n <h2 class=\"eds-h2\">Peak hours</h2>\n <span class=\"eds-label\">\n @if (data()!.peakHours.timezone) {\n {{ data()!.peakHours.timezone }}\n } @else {\n Calls per hour\n }\n </span>\n </div>\n <div class=\"analytics-page__volume-chart analytics-page__volume-chart--peak\">\n <div class=\"analytics-page__plot\">\n <div class=\"analytics-page__grid\" aria-hidden=\"true\"></div>\n <div class=\"analytics-page__bars analytics-page__bars--peak\" role=\"list\" aria-label=\"Call volume by hour\">\n @for (point of data()!.peakHours.points; track point.hour) {\n <div\n class=\"analytics-page__bar-col analytics-page__bar-col--peak\"\n role=\"listitem\"\n [attr.aria-label]=\"peakTooltip(point)\"\n >\n <div class=\"analytics-page__bar-wrap\">\n @if (point.count > 0) {\n <div\n class=\"analytics-page__bar-stack\"\n [style.height.%]=\"barHeightPercent(point, data()!.peakHours.points)\"\n >\n <div class=\"analytics-page__bar\"></div>\n </div>\n }\n <span class=\"analytics-page__tooltip\">{{ peakTooltip(point) }}</span>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n <div class=\"analytics-page__peak-axis\">\n @for (point of data()!.peakHours.points; track point.hour; let i = $index) {\n <span class=\"analytics-page__peak-tick eds-mono-sm\">\n @if (showPeakAxisLabel(i, data()!.peakHours.points.length)) {\n {{ point.label }}\n }\n </span>\n }\n </div>\n </article>\n </div>\n }\n</div>\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { DatePipe } from '@angular/common';
|
|
4
|
+
import * as i1 from '@angular/forms';
|
|
5
|
+
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import { A as AiReceptionistApiService, E as EdsButtonComponent, b as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-DClMfxeY.mjs';
|
|
7
|
+
|
|
8
|
+
class AuditPageComponent {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.api = inject(AiReceptionistApiService);
|
|
11
|
+
this.events = signal([]);
|
|
12
|
+
this.search = signal('');
|
|
13
|
+
this.actor = signal('');
|
|
14
|
+
this.loading = signal(true);
|
|
15
|
+
}
|
|
16
|
+
ngOnInit() {
|
|
17
|
+
this.load();
|
|
18
|
+
}
|
|
19
|
+
load() {
|
|
20
|
+
this.loading.set(true);
|
|
21
|
+
this.api.getAudit(this.search() || undefined).subscribe(({ data }) => {
|
|
22
|
+
this.events.set(data);
|
|
23
|
+
this.loading.set(false);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AuditPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
27
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: AuditPageComponent, isStandalone: true, selector: "eds-audit-page", ngImport: i0, template: "<div class=\"eds-page audit-page\">\n <header class=\"audit-page__header\">\n <div>\n <h1 class=\"eds-h1\">Audit log</h1>\n <p class=\"eds-body-sm\">Track configuration and agent activity</p>\n </div>\n </header>\n\n <div class=\"eds-card audit-page__filters\">\n <input\n type=\"search\"\n class=\"audit-page__input\"\n placeholder=\"Search action, target, detail\u2026\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <input\n type=\"text\"\n class=\"audit-page__input\"\n placeholder=\"Filter by actor\"\n [ngModel]=\"actor()\"\n (ngModelChange)=\"actor.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <eds-button variant=\"primary\" (click)=\"load()\">\n <eds-icon name=\"search\" [size]=\"18\" /> Search\n </eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading audit events\u2026</p>\n } @else {\n <div class=\"eds-card audit-page__table-wrap\">\n <table class=\"eds-table\">\n <thead>\n <tr>\n <th>When</th>\n <th>Actor</th>\n <th>Action</th>\n <th>Target</th>\n <th>Detail</th>\n </tr>\n </thead>\n <tbody>\n @for (event of events(); track event.id) {\n <tr>\n <td class=\"eds-mono-sm\">{{ event.at | date: 'medium' }}</td>\n <td>{{ event.actor }}</td>\n <td class=\"eds-body-sm-bold\">{{ event.action }}</td>\n <td class=\"eds-mono-sm\">{{ event.target }}</td>\n <td>{{ event.detail }}</td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"5\" class=\"audit-page__empty\">No events match your filters.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n</div>\n", styles: [".audit-page__header{margin-bottom:20px}.audit-page__filters{display:flex;flex-wrap:wrap;gap:10px;padding:14px;margin-bottom:16px;align-items:center}.audit-page__input{flex:1;min-width:180px;padding:8px 12px;border:1px solid var(--border-default);border-radius:var(--eds-radius-md);font-size:14px}.audit-page__table-wrap{overflow-x:auto;padding:0}.audit-page__empty{text-align:center;padding:32px!important;color:var(--text-secondary)}\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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
28
|
+
}
|
|
29
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: AuditPageComponent, decorators: [{
|
|
30
|
+
type: Component,
|
|
31
|
+
args: [{ selector: 'eds-audit-page', imports: [DatePipe, FormsModule, EdsButtonComponent, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page audit-page\">\n <header class=\"audit-page__header\">\n <div>\n <h1 class=\"eds-h1\">Audit log</h1>\n <p class=\"eds-body-sm\">Track configuration and agent activity</p>\n </div>\n </header>\n\n <div class=\"eds-card audit-page__filters\">\n <input\n type=\"search\"\n class=\"audit-page__input\"\n placeholder=\"Search action, target, detail\u2026\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <input\n type=\"text\"\n class=\"audit-page__input\"\n placeholder=\"Filter by actor\"\n [ngModel]=\"actor()\"\n (ngModelChange)=\"actor.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <eds-button variant=\"primary\" (click)=\"load()\">\n <eds-icon name=\"search\" [size]=\"18\" /> Search\n </eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading audit events\u2026</p>\n } @else {\n <div class=\"eds-card audit-page__table-wrap\">\n <table class=\"eds-table\">\n <thead>\n <tr>\n <th>When</th>\n <th>Actor</th>\n <th>Action</th>\n <th>Target</th>\n <th>Detail</th>\n </tr>\n </thead>\n <tbody>\n @for (event of events(); track event.id) {\n <tr>\n <td class=\"eds-mono-sm\">{{ event.at | date: 'medium' }}</td>\n <td>{{ event.actor }}</td>\n <td class=\"eds-body-sm-bold\">{{ event.action }}</td>\n <td class=\"eds-mono-sm\">{{ event.target }}</td>\n <td>{{ event.detail }}</td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"5\" class=\"audit-page__empty\">No events match your filters.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n</div>\n", styles: [".audit-page__header{margin-bottom:20px}.audit-page__filters{display:flex;flex-wrap:wrap;gap:10px;padding:14px;margin-bottom:16px;align-items:center}.audit-page__input{flex:1;min-width:180px;padding:8px 12px;border:1px solid var(--border-default);border-radius:var(--eds-radius-md);font-size:14px}.audit-page__table-wrap{overflow-x:auto;padding:0}.audit-page__empty{text-align:center;padding:32px!important;color:var(--text-secondary)}\n"] }]
|
|
32
|
+
}] });
|
|
33
|
+
|
|
34
|
+
export { AuditPageComponent };
|
|
35
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edvak99-voice-receptionist-widget-audit-page.component-ou9oxnJp.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/pages/audit-page/audit-page.component.ts","../../../projects/voice-receptionist-widget/src/lib/pages/audit-page/audit-page.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\nimport { AuditEvent } from '../../models/ai-receptionist.types';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { EdsButtonComponent } from '../../ui/eds-button.component';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\n\n@Component({\n selector: 'eds-audit-page',\n imports: [DatePipe, FormsModule, EdsButtonComponent, EdsIconComponent],\n templateUrl: './audit-page.component.html',\n styleUrl: './audit-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class AuditPageComponent implements OnInit {\n private readonly api = inject(AiReceptionistApiService);\n\n protected readonly events = signal<AuditEvent[]>([]);\n protected readonly search = signal('');\n protected readonly actor = signal('');\n protected readonly loading = signal(true);\n\n ngOnInit(): void {\n this.load();\n }\n\n protected load(): void {\n this.loading.set(true);\n this.api.getAudit(this.search() || undefined).subscribe(({ data }) => {\n this.events.set(data);\n this.loading.set(false);\n });\n }\n}\n","<div class=\"eds-page audit-page\">\n <header class=\"audit-page__header\">\n <div>\n <h1 class=\"eds-h1\">Audit log</h1>\n <p class=\"eds-body-sm\">Track configuration and agent activity</p>\n </div>\n </header>\n\n <div class=\"eds-card audit-page__filters\">\n <input\n type=\"search\"\n class=\"audit-page__input\"\n placeholder=\"Search action, target, detail…\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <input\n type=\"text\"\n class=\"audit-page__input\"\n placeholder=\"Filter by actor\"\n [ngModel]=\"actor()\"\n (ngModelChange)=\"actor.set($event)\"\n (keyup.enter)=\"load()\"\n />\n <eds-button variant=\"primary\" (click)=\"load()\">\n <eds-icon name=\"search\" [size]=\"18\" /> Search\n </eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading audit events…</p>\n } @else {\n <div class=\"eds-card audit-page__table-wrap\">\n <table class=\"eds-table\">\n <thead>\n <tr>\n <th>When</th>\n <th>Actor</th>\n <th>Action</th>\n <th>Target</th>\n <th>Detail</th>\n </tr>\n </thead>\n <tbody>\n @for (event of events(); track event.id) {\n <tr>\n <td class=\"eds-mono-sm\">{{ event.at | date: 'medium' }}</td>\n <td>{{ event.actor }}</td>\n <td class=\"eds-body-sm-bold\">{{ event.action }}</td>\n <td class=\"eds-mono-sm\">{{ event.target }}</td>\n <td>{{ event.detail }}</td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"5\" class=\"audit-page__empty\">No events match your filters.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n</div>\n"],"names":[],"mappings":";;;;;;;MAgBa,kBAAkB,CAAA;AAP/B,IAAA,WAAA,GAAA;AAQmB,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC;AAEpC,QAAA,IAAA,CAAA,MAAM,GAAG,MAAM,CAAe,EAAE,CAAC;AACjC,QAAA,IAAA,CAAA,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;AACnB,QAAA,IAAA,CAAA,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC;AAClB,QAAA,IAAA,CAAA,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;AAa1C,IAAA;IAXC,QAAQ,GAAA;QACN,IAAI,CAAC,IAAI,EAAE;IACb;IAEU,IAAI,GAAA;AACZ,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,KAAI;AACnE,YAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;AACrB,YAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACzB,QAAA,CAAC,CAAC;IACJ;+GAlBW,kBAAkB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;mGAAlB,kBAAkB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,gBAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EChB/B,i4DA+DA,EAAA,MAAA,EAAA,CAAA,sbAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EDpDc,QAAQ,4CAAE,WAAW,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,8MAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,eAAA,EAAA,QAAA,EAAA,2CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,qDAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,UAAA,EAAA,SAAA,EAAA,gBAAA,CAAA,EAAA,OAAA,EAAA,CAAA,eAAA,CAAA,EAAA,QAAA,EAAA,CAAA,SAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,kBAAkB,EAAA,QAAA,EAAA,YAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,gBAAgB,EAAA,QAAA,EAAA,UAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA,CAAA;;4FAK5D,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAP9B,SAAS;AACI,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,gBAAgB,EAAA,OAAA,EACjB,CAAC,QAAQ,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,CAAC,EAAA,eAAA,EAGrD,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,i4DAAA,EAAA,MAAA,EAAA,CAAA,sbAAA,CAAA,EAAA;;;;;"}
|