@aglyn/plugins-marketing 1.0.0-beta.143

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 (183) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +56 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
  8. package/src/lib/components/announcement-bar-card.component.js +292 -0
  9. package/src/lib/components/announcement-bar-card.component.js.map +1 -0
  10. package/src/lib/components/campaign-composer.d.ts +84 -0
  11. package/src/lib/components/campaign-composer.js +1643 -0
  12. package/src/lib/components/campaign-composer.js.map +1 -0
  13. package/src/lib/components/campaign-conversions-card.d.ts +22 -0
  14. package/src/lib/components/campaign-conversions-card.js +642 -0
  15. package/src/lib/components/campaign-conversions-card.js.map +1 -0
  16. package/src/lib/components/campaign-detail-card.d.ts +55 -0
  17. package/src/lib/components/campaign-detail-card.js +771 -0
  18. package/src/lib/components/campaign-detail-card.js.map +1 -0
  19. package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
  20. package/src/lib/components/campaign-edit-drawer.js +252 -0
  21. package/src/lib/components/campaign-edit-drawer.js.map +1 -0
  22. package/src/lib/components/campaign-email-zones.d.ts +111 -0
  23. package/src/lib/components/campaign-email-zones.js +54 -0
  24. package/src/lib/components/campaign-email-zones.js.map +1 -0
  25. package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
  26. package/src/lib/components/campaign-glance-card.component.js +151 -0
  27. package/src/lib/components/campaign-glance-card.component.js.map +1 -0
  28. package/src/lib/components/campaign-members-section.d.ts +87 -0
  29. package/src/lib/components/campaign-members-section.js +499 -0
  30. package/src/lib/components/campaign-members-section.js.map +1 -0
  31. package/src/lib/components/campaign-reach-sections.d.ts +137 -0
  32. package/src/lib/components/campaign-reach-sections.js +694 -0
  33. package/src/lib/components/campaign-reach-sections.js.map +1 -0
  34. package/src/lib/components/campaign-report-card.d.ts +91 -0
  35. package/src/lib/components/campaign-report-card.js +667 -0
  36. package/src/lib/components/campaign-report-card.js.map +1 -0
  37. package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
  38. package/src/lib/components/campaign-test-send-drawer.js +238 -0
  39. package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
  40. package/src/lib/components/campaigns-card.d.ts +31 -0
  41. package/src/lib/components/campaigns-card.js +667 -0
  42. package/src/lib/components/campaigns-card.js.map +1 -0
  43. package/src/lib/components/conversion-attribution.component.d.ts +62 -0
  44. package/src/lib/components/conversion-attribution.component.js +127 -0
  45. package/src/lib/components/conversion-attribution.component.js.map +1 -0
  46. package/src/lib/components/email-compose-card.d.ts +12 -0
  47. package/src/lib/components/email-compose-card.js +157 -0
  48. package/src/lib/components/email-compose-card.js.map +1 -0
  49. package/src/lib/components/email-detail.d.ts +40 -0
  50. package/src/lib/components/email-detail.js +1180 -0
  51. package/src/lib/components/email-detail.js.map +1 -0
  52. package/src/lib/components/email-edit-drawer.d.ts +54 -0
  53. package/src/lib/components/email-edit-drawer.js +172 -0
  54. package/src/lib/components/email-edit-drawer.js.map +1 -0
  55. package/src/lib/components/email-messages-widget.d.ts +22 -0
  56. package/src/lib/components/email-messages-widget.js +49 -0
  57. package/src/lib/components/email-messages-widget.js.map +1 -0
  58. package/src/lib/components/email-recipients-card.d.ts +50 -0
  59. package/src/lib/components/email-recipients-card.js +338 -0
  60. package/src/lib/components/email-recipients-card.js.map +1 -0
  61. package/src/lib/components/emails-list-card.d.ts +36 -0
  62. package/src/lib/components/emails-list-card.js +544 -0
  63. package/src/lib/components/emails-list-card.js.map +1 -0
  64. package/src/lib/components/experiment-zones.d.ts +78 -0
  65. package/src/lib/components/experiment-zones.js +65 -0
  66. package/src/lib/components/experiment-zones.js.map +1 -0
  67. package/src/lib/components/host-experiments-card.component.d.ts +17 -0
  68. package/src/lib/components/host-experiments-card.component.js +887 -0
  69. package/src/lib/components/host-experiments-card.component.js.map +1 -0
  70. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  71. package/src/lib/components/host-marketing-summary-card.component.js +161 -0
  72. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
  73. package/src/lib/components/host-overlays-card.component.d.ts +19 -0
  74. package/src/lib/components/host-overlays-card.component.js +783 -0
  75. package/src/lib/components/host-overlays-card.component.js.map +1 -0
  76. package/src/lib/components/marketing-console-page.d.ts +28 -0
  77. package/src/lib/components/marketing-console-page.js +166 -0
  78. package/src/lib/components/marketing-console-page.js.map +1 -0
  79. package/src/lib/components/marketing-console-sections.d.ts +34 -0
  80. package/src/lib/components/marketing-console-sections.js +77 -0
  81. package/src/lib/components/marketing-console-sections.js.map +1 -0
  82. package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
  83. package/src/lib/components/overlay-stats-row.component.js +78 -0
  84. package/src/lib/components/overlay-stats-row.component.js.map +1 -0
  85. package/src/lib/components/popup-card.component.d.ts +18 -0
  86. package/src/lib/components/popup-card.component.js +462 -0
  87. package/src/lib/components/popup-card.component.js.map +1 -0
  88. package/src/lib/components/record-attribution-widget.d.ts +35 -0
  89. package/src/lib/components/record-attribution-widget.js +33 -0
  90. package/src/lib/components/record-attribution-widget.js.map +1 -0
  91. package/src/lib/components/site-runtime.d.ts +9 -0
  92. package/src/lib/components/site-runtime.js +1081 -0
  93. package/src/lib/components/site-runtime.js.map +1 -0
  94. package/src/lib/components/use-campaign-send-api.d.ts +35 -0
  95. package/src/lib/components/use-campaign-send-api.js +99 -0
  96. package/src/lib/components/use-campaign-send-api.js.map +1 -0
  97. package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
  98. package/src/lib/components/use-campaign-topic-options.js +44 -0
  99. package/src/lib/components/use-campaign-topic-options.js.map +1 -0
  100. package/src/lib/components/use-emails-hub-path.d.ts +39 -0
  101. package/src/lib/components/use-emails-hub-path.js +70 -0
  102. package/src/lib/components/use-emails-hub-path.js.map +1 -0
  103. package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
  104. package/src/lib/components/use-marketing-hub-path.js +50 -0
  105. package/src/lib/components/use-marketing-hub-path.js.map +1 -0
  106. package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
  107. package/src/lib/components/use-overlay-copy-editor.js +110 -0
  108. package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
  109. package/src/lib/constants/bundle-common.d.ts +8 -0
  110. package/src/lib/constants/bundle-common.js +9 -0
  111. package/src/lib/constants/bundle-common.js.map +1 -0
  112. package/src/lib/model/campaign-membership-figures.d.ts +109 -0
  113. package/src/lib/model/campaign-membership-figures.js +113 -0
  114. package/src/lib/model/campaign-membership-figures.js.map +1 -0
  115. package/src/lib/model/compile-client-automations.d.ts +65 -0
  116. package/src/lib/model/compile-client-automations.js +95 -0
  117. package/src/lib/model/compile-client-automations.js.map +1 -0
  118. package/src/lib/model/experiments.d.ts +130 -0
  119. package/src/lib/model/experiments.js +186 -0
  120. package/src/lib/model/experiments.js.map +1 -0
  121. package/src/lib/model/index.d.ts +26 -0
  122. package/src/lib/model/index.js +26 -0
  123. package/src/lib/model/index.js.map +1 -0
  124. package/src/lib/model/marketing-record-routes.d.ts +6 -0
  125. package/src/lib/model/marketing-record-routes.js +46 -0
  126. package/src/lib/model/marketing-record-routes.js.map +1 -0
  127. package/src/lib/model/overlay-copy.d.ts +62 -0
  128. package/src/lib/model/overlay-copy.js +78 -0
  129. package/src/lib/model/overlay-copy.js.map +1 -0
  130. package/src/lib/model/overlays.d.ts +113 -0
  131. package/src/lib/model/overlays.js +93 -0
  132. package/src/lib/model/overlays.js.map +1 -0
  133. package/src/lib/model/site-contract.d.ts +100 -0
  134. package/src/lib/model/site-contract.js +23 -0
  135. package/src/lib/model/site-contract.js.map +1 -0
  136. package/src/lib/plugin.d.ts +27 -0
  137. package/src/lib/plugin.js +168 -0
  138. package/src/lib/plugin.js.map +1 -0
  139. package/src/lib/preview-props.d.ts +31 -0
  140. package/src/lib/preview-props.js +54 -0
  141. package/src/lib/preview-props.js.map +1 -0
  142. package/src/lib/server/campaign-manage.d.ts +62 -0
  143. package/src/lib/server/campaign-manage.js +829 -0
  144. package/src/lib/server/campaign-manage.js.map +1 -0
  145. package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
  146. package/src/lib/server/campaign-process-scheduled.js +198 -0
  147. package/src/lib/server/campaign-process-scheduled.js.map +1 -0
  148. package/src/lib/server/campaign-recipients.d.ts +55 -0
  149. package/src/lib/server/campaign-recipients.js +219 -0
  150. package/src/lib/server/campaign-recipients.js.map +1 -0
  151. package/src/lib/server/campaign-send.d.ts +487 -0
  152. package/src/lib/server/campaign-send.js +3157 -0
  153. package/src/lib/server/campaign-send.js.map +1 -0
  154. package/src/lib/server/email-campaign-reach.d.ts +174 -0
  155. package/src/lib/server/email-campaign-reach.js +254 -0
  156. package/src/lib/server/email-campaign-reach.js.map +1 -0
  157. package/src/lib/server/email-events.d.ts +27 -0
  158. package/src/lib/server/email-events.js +714 -0
  159. package/src/lib/server/email-events.js.map +1 -0
  160. package/src/lib/server/get-client-automations.d.ts +60 -0
  161. package/src/lib/server/get-client-automations.js +87 -0
  162. package/src/lib/server/get-client-automations.js.map +1 -0
  163. package/src/lib/server/get-overlays.d.ts +28 -0
  164. package/src/lib/server/get-overlays.js +65 -0
  165. package/src/lib/server/get-overlays.js.map +1 -0
  166. package/src/lib/server/get-screen-experiments.d.ts +42 -0
  167. package/src/lib/server/get-screen-experiments.js +82 -0
  168. package/src/lib/server/get-screen-experiments.js.map +1 -0
  169. package/src/lib/server/lists-materialize.d.ts +18 -0
  170. package/src/lib/server/lists-materialize.js +130 -0
  171. package/src/lib/server/lists-materialize.js.map +1 -0
  172. package/src/lib/server/marketing-figures.d.ts +37 -0
  173. package/src/lib/server/marketing-figures.js +248 -0
  174. package/src/lib/server/marketing-figures.js.map +1 -0
  175. package/src/lib/server/site-page-enricher.d.ts +26 -0
  176. package/src/lib/server/site-page-enricher.js +210 -0
  177. package/src/lib/server/site-page-enricher.js.map +1 -0
  178. package/src/lib/server.d.ts +20 -0
  179. package/src/lib/server.js +152 -0
  180. package/src/lib/server.js.map +1 -0
  181. package/src/lib/site.d.ts +22 -0
  182. package/src/lib/site.js +48 -0
  183. package/src/lib/site.js.map +1 -0
@@ -0,0 +1,887 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
19
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
20
+ import { checkEntitlement, createResourceUid, HOST_EVENT_TYPES, pluginDocsHelp, SITE_EVENT_TYPES } from "@aglyn/aglyn";
21
+ import { compareVariants, summarizeVariantStats, validateExperiment } from "../model/index.js";
22
+ import { mdiChartBar, mdiDeleteOutline, mdiPause, mdiPencilOutline, mdiPlay } from "@aglyn/shared-data-mdi";
23
+ import { CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
24
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
25
+ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
26
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
27
+ import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
28
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
29
+ import { Timestamp } from "@aglyn/shared-util-timestamp";
30
+ import { Alert, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, MenuItem, Stack, Switch, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@mui/material";
31
+ import { collection, deleteDoc, doc, getDocs, limit, orderBy, query, setDoc } from "firebase/firestore";
32
+ import { useState } from "react";
33
+ import { useFirestore, useFirestoreCollection, useHostActivityLogger, usePagedCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
34
+ import { ExperimentResultZone, ExperimentVariantsZone, marketingExperimentVariantDrafts } from "./experiment-zones.js";
35
+ const STATUS_COLORS = {
36
+ draft: 'default',
37
+ running: 'success',
38
+ paused: 'warning',
39
+ done: 'info'
40
+ };
41
+ /**
42
+ * Experiments manager (AGL-252): create screen/section/email A/B tests
43
+ * with weighted variants and a conversion goal; start/pause/finish them
44
+ * and read exposures/conversions per variant. Business tier
45
+ * (`ab-testing`).
46
+ */ export function HostExperimentsCard(props) {
47
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _editor_$id, _editor_name, _editor_target, _ref6, _ref7, _ref8, _results_experiment_$id, _results_experiment_$id1;
48
+ var _editor_goal, _editor_goal1, _results_experiment_name;
49
+ const { hostId } = props;
50
+ const firestore = useFirestore();
51
+ const { org } = props;
52
+ const { enqueueSnackbar } = useSnackbar();
53
+ const { confirm } = useConfirmationContext();
54
+ const logActivity = useHostActivityLogger(hostId);
55
+ const entitled = checkEntitlement(org, 'abTesting');
56
+ const { rows: experimentRows, hasMore: hasMoreExperiments, page: experimentPage, setPage: setExperimentPage, pageSize: experimentPageSize, setPageSize: setExperimentPageSize, status: experimentsStatus, /**
57
+ * The rows this editor is seeded from are unconfirmed by the server
58
+ * (AGL-1358). Editing spreads a whole stored experiment into `editor`
59
+ * and writes all of it back, so `merge: true` protects nothing — every
60
+ * field is in the payload. `status` is the one that bites: it is
61
+ * otherwise only ever moved by the start/pause/finish controls below, so
62
+ * a cached seed can quietly restart a finished test, or stop a running
63
+ * one, along with reverting its variant weights and goal.
64
+ */ fromCache: experimentsFromCache } = usePagedCollection((pageLimit)=>query(collection(firestore, 'hosts', hostId, 'experiments'), /*
65
+ * Ordered by the server and paged (AGL-2501, AGL-2292).
66
+ *
67
+ * `limit(50)` with no `orderBy` is answered in DOCUMENT-ID order over
68
+ * ids from `createResourceUid()`, so the window was an arbitrary fifty
69
+ * that a `localeCompare` in the browser arranged into a convincing A-to-Z
70
+ * page. A site past fifty experiments could not reach the rest, and
71
+ * the ones missing left no gap on screen.
72
+ *
73
+ * `name` is safe to order on, checked against the writers rather than
74
+ * assumed: this card is the only thing that creates an experiment,
75
+ * `newExperiment()` seeds `name: ''` and `validateExperiment` refuses
76
+ * a save without one, the two server paths (`email-events`,
77
+ * `campaign-send`) only ever `update` an existing document, and
78
+ * `experiments` is not in `IMPORTABLE_FIELDS`. There is no writer that
79
+ * can produce a nameless experiment for `orderBy` to drop.
80
+ */ orderBy('name'), limit(pageLimit)), [
81
+ firestore,
82
+ hostId
83
+ ], {
84
+ idField: '$id'
85
+ });
86
+ /*
87
+ * Soft-deleted rows are dropped HERE and not in the query, which is why a
88
+ * page can render fewer rows than its size: Firestore cannot ask for
89
+ * documents that LACK a field, and a live experiment has no `deletedAt` at
90
+ * all. The rows are not re-sorted — the server already ordered them, and
91
+ * re-sorting a page of a name-ordered walk is the lie the old code told.
92
+ */ const experiments = experimentRows.filter((experiment)=>!experiment.deletedAt);
93
+ const { data: screenDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'screens'), limit(100)), [
94
+ firestore,
95
+ hostId
96
+ ], {
97
+ idField: '$id'
98
+ });
99
+ const screenOptions = [
100
+ ...screenDocs != null ? screenDocs : []
101
+ ].filter((screen)=>!screen.deletedAt).map((screen)=>{
102
+ var _ref, _screen_displayName;
103
+ return {
104
+ id: screen.$id,
105
+ name: (_ref = (_screen_displayName = screen.displayName) != null ? _screen_displayName : screen.name) != null ? _ref : screen.$id
106
+ };
107
+ }).sort((a, b)=>a.name.localeCompare(b.name));
108
+ const [editor, setEditor] = useState(null);
109
+ // Versions of the screen under test (AGL-253): variants pin one each.
110
+ const { data: versionDocs } = useFirestoreCollection(()=>{
111
+ var _ref;
112
+ return query(collection(firestore, 'hosts', hostId, 'screens', (_ref = editor == null ? void 0 : editor.screenId) != null ? _ref : '-none-', 'versions'), limit(50));
113
+ }, [
114
+ firestore,
115
+ hostId,
116
+ editor == null ? void 0 : editor.screenId
117
+ ], {
118
+ idField: '$id'
119
+ });
120
+ const versionOptions = [
121
+ ...versionDocs != null ? versionDocs : []
122
+ ].filter((version)=>!version.deletedAt).map((version)=>{
123
+ var _ref, _version_name;
124
+ return {
125
+ id: version.$id,
126
+ name: (_ref = (_version_name = version.name) != null ? _version_name : version.displayName) != null ? _ref : version.$id
127
+ };
128
+ }).sort((a, b)=>a.name.localeCompare(b.name));
129
+ const [results, setResults] = useState(null);
130
+ const patch = (partial)=>setEditor((previous)=>previous ? _extends({}, previous, partial) : previous);
131
+ /**
132
+ * What a widget in the variants zone proposes, put into the editor's own
133
+ * fields as unsaved changes. Only the variants the editor already holds are
134
+ * filled, in order: the test's shape — how many arms, their ids, their
135
+ * weights, the version each screen variant pins — is the person's, and a
136
+ * proposal that could add or drop an arm would be changing the test rather
137
+ * than writing copy for it. A field a widget left empty is left alone, so
138
+ * a proposal for an email's subject does not blank a body someone typed.
139
+ */ const proposeVariants = (proposed)=>setEditor((previous)=>previous ? _extends({}, previous, {
140
+ variants: previous.variants.map((variant, index)=>{
141
+ const draft = proposed[index];
142
+ if (!draft) return variant;
143
+ return _extends({}, variant, draft.name ? {
144
+ name: draft.name
145
+ } : {}, draft.subject ? {
146
+ subject: draft.subject
147
+ } : {}, draft.body ? {
148
+ body: draft.body
149
+ } : {});
150
+ })
151
+ }) : previous);
152
+ const patchVariant = (index, partial)=>setEditor((previous)=>previous ? _extends({}, previous, {
153
+ variants: previous.variants.map((variant, index2)=>index2 === index ? _extends({}, variant, partial) : variant)
154
+ }) : previous);
155
+ const newExperiment = ()=>({
156
+ name: '',
157
+ status: 'draft',
158
+ target: 'screen',
159
+ variants: [
160
+ {
161
+ id: 'a',
162
+ name: 'A (control)',
163
+ weight: 1
164
+ },
165
+ {
166
+ id: 'b',
167
+ name: 'B',
168
+ weight: 1
169
+ }
170
+ ],
171
+ goal: {
172
+ event: 'formSubmission'
173
+ }
174
+ });
175
+ const handleSave = async ()=>{
176
+ var _editor_$id;
177
+ if (!editor) return;
178
+ const problem = validateExperiment(editor);
179
+ if (problem) {
180
+ return void enqueueSnackbar(problem, {
181
+ variant: 'warning',
182
+ persist: false
183
+ });
184
+ }
185
+ const id = (_editor_$id = editor.$id) != null ? _editor_$id : createResourceUid();
186
+ const { $id: _ignored } = editor, payload = _object_without_properties_loose(editor, [
187
+ "$id"
188
+ ]);
189
+ try {
190
+ /**
191
+ * Refuse an EDIT whose seed the server never confirmed (AGL-1358).
192
+ *
193
+ * `merge: true` is here so a cleared end date or auto-winner overwrites
194
+ * (AGL-273), not to protect untouched fields — there are none, because
195
+ * the payload is the whole stored row spread into `editor`. `status`
196
+ * and `winnerVariantId` ride along with it, and those are otherwise
197
+ * only moved by `setStatus`, so a cached seed can restart a test the
198
+ * owner finished or halt one that is splitting live traffic.
199
+ *
200
+ * Only the edit path. A NEW experiment comes from `newExperiment()` at
201
+ * a fresh uid and can overwrite nothing, and the first snapshot of any
202
+ * listener is `fromCache: true`, so guarding a create would refuse a
203
+ * save that was never unsafe.
204
+ *
205
+ * The guard WRAPS the write, and the activity log with it: a logged
206
+ * "Updated experiment" whose write never happened leaves the history
207
+ * disagreeing with the experiment it claims to explain.
208
+ */ const verdict = await writeGuardedBySeed({
209
+ subject: 'experiment',
210
+ unreadable: Boolean(editor.$id) && experimentsStatus === 'error',
211
+ fromCache: Boolean(editor.$id) && experimentsFromCache
212
+ }, async ()=>{
213
+ var _editor_endAtMs, _editor_autoWinner;
214
+ await setDoc(doc(firestore, 'hosts', hostId, 'experiments', id), _extends({}, JSON.parse(JSON.stringify(payload)), {
215
+ // Clearing the end date / auto-winner must overwrite — a
216
+ // merge-set keeps absent keys otherwise (AGL-273).
217
+ endAtMs: (_editor_endAtMs = editor.endAtMs) != null ? _editor_endAtMs : null,
218
+ autoWinner: (_editor_autoWinner = editor.autoWinner) != null ? _editor_autoWinner : null,
219
+ updatedAt: Timestamp.now()
220
+ }, editor.$id ? {} : {
221
+ createdAt: Timestamp.now()
222
+ }), {
223
+ merge: true
224
+ });
225
+ logActivity(editor.$id ? 'Updated experiment' : 'Created experiment', {
226
+ type: 'content',
227
+ id,
228
+ name: editor.name
229
+ });
230
+ });
231
+ // A refusal keeps the editor open with everything that was typed.
232
+ if (!verdict.ok) {
233
+ return void enqueueSnackbar(verdict.message, {
234
+ variant: 'warning',
235
+ persist: false
236
+ });
237
+ }
238
+ enqueueSnackbar('Experiment saved', {
239
+ variant: 'success',
240
+ persist: false
241
+ });
242
+ setEditor(null);
243
+ } catch (error) {
244
+ console.error(error);
245
+ enqueueSnackbar('An error has occurred', {
246
+ variant: 'error'
247
+ });
248
+ }
249
+ };
250
+ const setStatus = async (experiment, status, winnerVariantId)=>{
251
+ if (!experiment.$id) return;
252
+ // One running experiment per screen (AGL-265): the page runner only
253
+ // serves the first, so a second would silently never split traffic.
254
+ if (status === 'running' && experiment.screenId) {
255
+ const clash = experiments.find((candidate)=>candidate.$id !== experiment.$id && candidate.status === 'running' && candidate.screenId === experiment.screenId);
256
+ if (clash) {
257
+ return void enqueueSnackbar(`"${clash.name}" is already running on that screen — pause or ` + 'finish it first', {
258
+ variant: 'warning',
259
+ persist: false
260
+ });
261
+ }
262
+ }
263
+ await setDoc(doc(firestore, 'hosts', hostId, 'experiments', experiment.$id), _extends({
264
+ status
265
+ }, winnerVariantId ? {
266
+ winnerVariantId
267
+ } : {}, {
268
+ updatedAt: Timestamp.now()
269
+ }), {
270
+ merge: true
271
+ });
272
+ logActivity(`Experiment ${status}`, {
273
+ type: 'content',
274
+ id: experiment.$id,
275
+ name: experiment.name
276
+ });
277
+ };
278
+ const handleDelete = async (experiment)=>{
279
+ if (!experiment.$id) return;
280
+ const accepted = await confirm({
281
+ title: 'Delete experiment?',
282
+ description: `"${experiment.name}" and its results are removed.`,
283
+ confirmationText: 'Delete',
284
+ confirmationButtonProps: {
285
+ color: 'error'
286
+ }
287
+ })// Resolves with no value and rejects on cancel — gating on the
288
+ // resolved value alone made this always return (AGL-950).
289
+ .then(()=>true).catch(()=>false);
290
+ if (!accepted) return;
291
+ await deleteDoc(doc(firestore, 'hosts', hostId, 'experiments', experiment.$id));
292
+ };
293
+ const openResults = async (experiment)=>{
294
+ if (!experiment.$id) return;
295
+ const snapshot = await getDocs(collection(firestore, 'hosts', hostId, 'experiments', experiment.$id, 'stats'));
296
+ const stats = {};
297
+ snapshot.forEach((entry)=>{
298
+ stats[entry.id] = entry.data();
299
+ });
300
+ setResults({
301
+ experiment,
302
+ stats
303
+ });
304
+ };
305
+ /*
306
+ * One row per experiment. The row opens its results, the one thing every
307
+ * experiment has to show; starting, pausing, editing and deleting it are in
308
+ * the row's menu rather than a row of buttons one mis-click from each other.
309
+ */ const experimentActions = (experiment)=>[
310
+ ...experiment.status === 'running' ? [
311
+ {
312
+ key: 'pause',
313
+ label: 'Pause',
314
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
315
+ path: mdiPause.path,
316
+ size: 0.8
317
+ }),
318
+ onClick: ()=>void setStatus(experiment, 'paused')
319
+ }
320
+ ] : experiment.status !== 'done' ? [
321
+ {
322
+ key: 'start',
323
+ label: 'Start',
324
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
325
+ path: mdiPlay.path,
326
+ size: 0.8
327
+ }),
328
+ onClick: ()=>void setStatus(experiment, 'running')
329
+ }
330
+ ] : [],
331
+ {
332
+ key: 'edit',
333
+ label: 'Edit',
334
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
335
+ path: mdiPencilOutline.path,
336
+ size: 0.8
337
+ }),
338
+ onClick: ()=>setEditor(_extends({}, experiment))
339
+ },
340
+ {
341
+ key: 'delete',
342
+ label: 'Delete',
343
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
344
+ path: mdiDeleteOutline.path,
345
+ size: 0.8
346
+ }),
347
+ destructive: true,
348
+ onClick: ()=>void handleDelete(experiment)
349
+ }
350
+ ];
351
+ const experimentColumns = [
352
+ {
353
+ field: 'name',
354
+ headerName: 'Experiment',
355
+ flex: 1,
356
+ minWidth: 200
357
+ },
358
+ {
359
+ field: 'target',
360
+ headerName: 'Tests',
361
+ flex: 1,
362
+ minWidth: 200,
363
+ valueGetter: (_value, experiment)=>{
364
+ var _experiment_variants;
365
+ return `${experiment.target} · ${((_experiment_variants = experiment.variants) != null ? _experiment_variants : []).length} variants`;
366
+ }
367
+ },
368
+ {
369
+ field: 'status',
370
+ headerName: 'Status',
371
+ width: 130,
372
+ renderCell: ({ row: experiment })=>{
373
+ var _STATUS_COLORS_experiment_status;
374
+ return /*#__PURE__*/ _jsx(Chip, {
375
+ size: "small",
376
+ color: (_STATUS_COLORS_experiment_status = STATUS_COLORS[experiment.status]) != null ? _STATUS_COLORS_experiment_status : 'default',
377
+ label: experiment.status
378
+ });
379
+ }
380
+ },
381
+ listActionsColumn((experiment)=>/*#__PURE__*/ _jsx(ListRowActions, {
382
+ label: experiment.name || 'this experiment',
383
+ quick: {
384
+ icon: mdiChartBar.path,
385
+ label: 'Results',
386
+ onClick: ()=>void openResults(experiment)
387
+ },
388
+ items: experimentActions(experiment)
389
+ }))
390
+ ];
391
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
392
+ header: "Experiments",
393
+ help: pluginDocsHelp('emailCampaigns', {
394
+ anchor: '#experiments'
395
+ }),
396
+ contentGutterX: true,
397
+ contentGutterY: true,
398
+ contentBordered: "all",
399
+ children: [
400
+ !entitled ? /*#__PURE__*/ _jsx(Alert, {
401
+ severity: "info",
402
+ children: 'A/B experiments are included in the Business plan — see ' + 'Billing to upgrade.'
403
+ }) : /*#__PURE__*/ _jsxs(Stack, {
404
+ spacing: 1.5,
405
+ children: [
406
+ /*#__PURE__*/ _jsx(Typography, {
407
+ variant: "body2",
408
+ color: "text.secondary",
409
+ children: 'Split traffic between variants of a screen, a section, or a ' + 'campaign email, and measure which one converts. Visitors ' + 'are assigned deterministically, so everyone keeps seeing ' + 'the same variant.'
410
+ }),
411
+ /*#__PURE__*/ _jsx(Button, {
412
+ size: "small",
413
+ variant: "contained",
414
+ color: "primary",
415
+ sx: {
416
+ alignSelf: 'flex-start'
417
+ },
418
+ onClick: ()=>setEditor(newExperiment()),
419
+ children: 'New experiment'
420
+ }),
421
+ experiments.length === 0 && !hasMoreExperiments ? null : /*#__PURE__*/ _jsxs(_Fragment, {
422
+ children: [
423
+ /*#__PURE__*/ _jsx(ListTable, {
424
+ "aria-label": "Experiments",
425
+ rows: experiments,
426
+ columns: experimentColumns,
427
+ rowHeight: TABLE_ROW_HEIGHT,
428
+ onOpen: (_id, experiment)=>void openResults(experiment),
429
+ // Paged by the footer below, so the grid must not also slice.
430
+ hideFooter: true
431
+ }),
432
+ /*#__PURE__*/ _jsx(ListPagination, {
433
+ page: experimentPage,
434
+ pageSize: experimentPageSize,
435
+ // The rows LEFT after the soft-delete filter, not the page the
436
+ // server returned: the footer must describe what is on screen.
437
+ rowCount: experiments.length,
438
+ hasMore: hasMoreExperiments,
439
+ onPageChange: setExperimentPage,
440
+ onPageSizeChange: setExperimentPageSize
441
+ })
442
+ ]
443
+ })
444
+ ]
445
+ }),
446
+ /*#__PURE__*/ _jsxs(Dialog, {
447
+ open: Boolean(editor),
448
+ onClose: ()=>setEditor(null),
449
+ maxWidth: "sm",
450
+ fullWidth: true,
451
+ children: [
452
+ /*#__PURE__*/ _jsx(DialogTitle, {
453
+ children: (editor == null ? void 0 : editor.$id) ? 'Edit experiment' : 'New experiment'
454
+ }),
455
+ /*#__PURE__*/ _jsxs(DialogContent, {
456
+ sx: {
457
+ display: 'flex',
458
+ flexDirection: 'column',
459
+ gap: 1.5,
460
+ pt: 1
461
+ },
462
+ children: [
463
+ /*#__PURE__*/ _jsx(TextField, {
464
+ size: "small",
465
+ label: "Name",
466
+ required: true,
467
+ value: (_ref = editor == null ? void 0 : editor.name) != null ? _ref : '',
468
+ onChange: (event)=>patch({
469
+ name: event.target.value
470
+ }),
471
+ sx: {
472
+ mt: 1
473
+ }
474
+ }),
475
+ /*#__PURE__*/ _jsxs(Stack, {
476
+ direction: "row",
477
+ spacing: 1,
478
+ children: [
479
+ /*#__PURE__*/ _jsxs(TextField, {
480
+ select: true,
481
+ size: "small",
482
+ label: "Tests",
483
+ value: (_ref1 = editor == null ? void 0 : editor.target) != null ? _ref1 : 'screen',
484
+ onChange: (event)=>patch({
485
+ target: event.target.value
486
+ }),
487
+ sx: {
488
+ minWidth: 140
489
+ },
490
+ children: [
491
+ /*#__PURE__*/ _jsx(MenuItem, {
492
+ value: "screen",
493
+ children: 'A screen'
494
+ }),
495
+ /*#__PURE__*/ _jsx(MenuItem, {
496
+ value: "section",
497
+ children: 'A section'
498
+ }),
499
+ /*#__PURE__*/ _jsx(MenuItem, {
500
+ value: "email",
501
+ children: 'An email'
502
+ })
503
+ ]
504
+ }),
505
+ (editor == null ? void 0 : editor.target) !== 'email' ? /*#__PURE__*/ _jsx(TextField, {
506
+ select: true,
507
+ size: "small",
508
+ label: "Screen",
509
+ value: (_ref2 = editor == null ? void 0 : editor.screenId) != null ? _ref2 : '',
510
+ onChange: (event)=>patch({
511
+ screenId: event.target.value
512
+ }),
513
+ sx: {
514
+ flex: 1
515
+ },
516
+ children: screenOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
517
+ value: option.id,
518
+ children: option.name
519
+ }, option.id))
520
+ }) : null
521
+ ]
522
+ }),
523
+ (editor == null ? void 0 : editor.target) === 'section' ? /*#__PURE__*/ _jsx(TextField, {
524
+ size: "small",
525
+ label: "Element id",
526
+ helperText: "The canvas element the variants swap (from the besigner)",
527
+ value: (_ref3 = editor == null ? void 0 : editor.nodeId) != null ? _ref3 : '',
528
+ onChange: (event)=>patch({
529
+ nodeId: event.target.value
530
+ })
531
+ }) : null,
532
+ /*#__PURE__*/ _jsx(TextField, {
533
+ select: true,
534
+ size: "small",
535
+ label: "Conversion goal",
536
+ value: (_ref4 = editor == null ? void 0 : (_editor_goal = editor.goal) == null ? void 0 : _editor_goal.event) != null ? _ref4 : 'formSubmission',
537
+ onChange: (event)=>patch({
538
+ goal: _extends({}, editor == null ? void 0 : editor.goal, {
539
+ event: event.target.value
540
+ })
541
+ }),
542
+ sx: {
543
+ maxWidth: 260
544
+ },
545
+ children: [
546
+ ...HOST_EVENT_TYPES,
547
+ ...SITE_EVENT_TYPES
548
+ ].map((eventType)=>/*#__PURE__*/ _jsx(MenuItem, {
549
+ value: eventType,
550
+ children: eventType
551
+ }, eventType))
552
+ }),
553
+ /*#__PURE__*/ _jsxs(Stack, {
554
+ direction: "row",
555
+ spacing: 1,
556
+ sx: {
557
+ alignItems: 'center',
558
+ flexWrap: 'wrap'
559
+ },
560
+ children: [
561
+ /*#__PURE__*/ _jsx(TextField, {
562
+ size: "small",
563
+ type: "datetime-local",
564
+ label: "Ends at (optional)",
565
+ slotProps: {
566
+ inputLabel: {
567
+ shrink: true
568
+ }
569
+ },
570
+ value: (editor == null ? void 0 : editor.endAtMs) ? new Date(editor.endAtMs - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16) : '',
571
+ onChange: (event)=>{
572
+ const ms = event.target.value ? new Date(event.target.value).getTime() : undefined;
573
+ patch({
574
+ endAtMs: Number.isFinite(ms) ? ms : undefined
575
+ });
576
+ },
577
+ helperText: "Past this, visitors get the default"
578
+ }),
579
+ /*#__PURE__*/ _jsx(FormControlLabel, {
580
+ control: /*#__PURE__*/ _jsx(Switch, {
581
+ size: "small",
582
+ checked: Boolean(editor == null ? void 0 : editor.autoWinner),
583
+ onChange: (event)=>patch({
584
+ autoWinner: event.target.checked ? {
585
+ minExposures: 200,
586
+ confidence: 0.95
587
+ } : undefined
588
+ })
589
+ }),
590
+ label: "Auto-declare winner"
591
+ }),
592
+ (editor == null ? void 0 : editor.autoWinner) ? /*#__PURE__*/ _jsxs(_Fragment, {
593
+ children: [
594
+ /*#__PURE__*/ _jsx(TextField, {
595
+ size: "small",
596
+ type: "number",
597
+ label: "Min exposures / variant",
598
+ value: editor.autoWinner.minExposures,
599
+ onChange: (event)=>patch({
600
+ autoWinner: _extends({}, editor.autoWinner, {
601
+ minExposures: Number(event.target.value)
602
+ })
603
+ }),
604
+ sx: {
605
+ width: 170
606
+ }
607
+ }),
608
+ /*#__PURE__*/ _jsxs(TextField, {
609
+ select: true,
610
+ size: "small",
611
+ label: "Confidence",
612
+ value: String(editor.autoWinner.confidence),
613
+ onChange: (event)=>patch({
614
+ autoWinner: _extends({}, editor.autoWinner, {
615
+ confidence: Number(event.target.value)
616
+ })
617
+ }),
618
+ sx: {
619
+ width: 120
620
+ },
621
+ children: [
622
+ /*#__PURE__*/ _jsx(MenuItem, {
623
+ value: "0.9",
624
+ children: '90%'
625
+ }),
626
+ /*#__PURE__*/ _jsx(MenuItem, {
627
+ value: "0.95",
628
+ children: '95%'
629
+ }),
630
+ /*#__PURE__*/ _jsx(MenuItem, {
631
+ value: "0.99",
632
+ children: '99%'
633
+ })
634
+ ]
635
+ })
636
+ ]
637
+ }) : null
638
+ ]
639
+ }),
640
+ /*#__PURE__*/ _jsx(Typography, {
641
+ variant: "subtitle2",
642
+ children: 'Variants'
643
+ }),
644
+ ((_ref5 = editor == null ? void 0 : editor.variants) != null ? _ref5 : []).map((variant, index)=>{
645
+ var _variant_name, _variant_weight, _variant_versionId, _variant_subject, _variant_body;
646
+ return /*#__PURE__*/ _jsxs(Stack, {
647
+ spacing: 1,
648
+ children: [
649
+ /*#__PURE__*/ _jsxs(Stack, {
650
+ direction: "row",
651
+ spacing: 1,
652
+ children: [
653
+ /*#__PURE__*/ _jsx(TextField, {
654
+ size: "small",
655
+ label: `Variant ${variant.id.toUpperCase()}`,
656
+ value: (_variant_name = variant.name) != null ? _variant_name : '',
657
+ onChange: (event)=>patchVariant(index, {
658
+ name: event.target.value
659
+ }),
660
+ sx: {
661
+ flex: 1
662
+ }
663
+ }),
664
+ /*#__PURE__*/ _jsx(TextField, {
665
+ size: "small",
666
+ type: "number",
667
+ label: "Weight",
668
+ value: (_variant_weight = variant.weight) != null ? _variant_weight : 1,
669
+ onChange: (event)=>patchVariant(index, {
670
+ weight: Number(event.target.value)
671
+ }),
672
+ sx: {
673
+ width: 100
674
+ }
675
+ }),
676
+ (editor == null ? void 0 : editor.target) !== 'email' ? // Screen/section variants pin a screen version
677
+ // (AGL-253); empty = the published version.
678
+ /*#__PURE__*/ _jsxs(TextField, {
679
+ select: true,
680
+ size: "small",
681
+ label: "Version",
682
+ value: (_variant_versionId = variant.versionId) != null ? _variant_versionId : '',
683
+ onChange: (event)=>patchVariant(index, {
684
+ versionId: event.target.value
685
+ }),
686
+ sx: {
687
+ minWidth: 160
688
+ },
689
+ children: [
690
+ /*#__PURE__*/ _jsx(MenuItem, {
691
+ value: "",
692
+ children: 'Published (control)'
693
+ }),
694
+ versionOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
695
+ value: option.id,
696
+ children: option.name
697
+ }, option.id))
698
+ ]
699
+ }) : null
700
+ ]
701
+ }),
702
+ (editor == null ? void 0 : editor.target) === 'email' ? // Email variants override the campaign copy (AGL-255).
703
+ /*#__PURE__*/ _jsxs(Stack, {
704
+ direction: "row",
705
+ spacing: 1,
706
+ children: [
707
+ /*#__PURE__*/ _jsx(TextField, {
708
+ size: "small",
709
+ label: "Subject override",
710
+ value: (_variant_subject = variant.subject) != null ? _variant_subject : '',
711
+ onChange: (event)=>patchVariant(index, {
712
+ subject: event.target.value
713
+ }),
714
+ sx: {
715
+ width: 220
716
+ }
717
+ }),
718
+ /*#__PURE__*/ _jsx(TextField, {
719
+ size: "small",
720
+ label: "Body override",
721
+ value: (_variant_body = variant.body) != null ? _variant_body : '',
722
+ onChange: (event)=>patchVariant(index, {
723
+ body: event.target.value
724
+ }),
725
+ multiline: true,
726
+ maxRows: 3,
727
+ sx: {
728
+ flex: 1
729
+ }
730
+ })
731
+ ]
732
+ }) : null
733
+ ]
734
+ }, variant.id);
735
+ }),
736
+ editor ? /*#__PURE__*/ _jsx(ExperimentVariantsZone, {
737
+ hostId: hostId,
738
+ experimentId: (_editor_$id = editor.$id) != null ? _editor_$id : '',
739
+ name: (_editor_name = editor.name) != null ? _editor_name : '',
740
+ target: (_editor_target = editor.target) != null ? _editor_target : 'screen',
741
+ goal: (_ref6 = (_editor_goal1 = editor.goal) == null ? void 0 : _editor_goal1.event) != null ? _ref6 : '',
742
+ variants: marketingExperimentVariantDrafts(editor.variants),
743
+ proposeVariants: proposeVariants
744
+ }) : null
745
+ ]
746
+ }),
747
+ /*#__PURE__*/ _jsxs(DialogActions, {
748
+ children: [
749
+ /*#__PURE__*/ _jsx(Button, {
750
+ color: "inherit",
751
+ onClick: ()=>setEditor(null),
752
+ children: 'Cancel'
753
+ }),
754
+ /*#__PURE__*/ _jsx(Button, {
755
+ variant: "contained",
756
+ color: "primary",
757
+ onClick: ()=>void handleSave(),
758
+ children: 'Save'
759
+ })
760
+ ]
761
+ })
762
+ ]
763
+ }),
764
+ /*#__PURE__*/ _jsxs(Dialog, {
765
+ open: Boolean(results),
766
+ onClose: ()=>setResults(null),
767
+ maxWidth: "xs",
768
+ fullWidth: true,
769
+ children: [
770
+ /*#__PURE__*/ _jsxs(DialogTitle, {
771
+ children: [
772
+ `Results — ${(_ref7 = results == null ? void 0 : results.experiment.name) != null ? _ref7 : ''}`,
773
+ (results == null ? void 0 : results.experiment.autoCompleted) ? // The track pipeline decided this one (AGL-273).
774
+ /*#__PURE__*/ _jsx(Chip, {
775
+ size: "small",
776
+ color: "success",
777
+ label: "Auto-completed",
778
+ sx: {
779
+ ml: 1
780
+ }
781
+ }) : null
782
+ ]
783
+ }),
784
+ /*#__PURE__*/ _jsxs(DialogContent, {
785
+ children: [
786
+ /*#__PURE__*/ _jsxs(ScrollTable, {
787
+ size: "small",
788
+ children: [
789
+ /*#__PURE__*/ _jsx(TableHead, {
790
+ children: /*#__PURE__*/ _jsxs(TableRow, {
791
+ children: [
792
+ /*#__PURE__*/ _jsx(TableCell, {
793
+ children: 'Variant'
794
+ }),
795
+ /*#__PURE__*/ _jsx(TableCell, {
796
+ children: 'Exposures'
797
+ }),
798
+ /*#__PURE__*/ _jsx(TableCell, {
799
+ children: 'Conversions'
800
+ }),
801
+ /*#__PURE__*/ _jsx(TableCell, {
802
+ children: 'Rate'
803
+ }),
804
+ /*#__PURE__*/ _jsx(TableCell, {
805
+ children: 'vs control'
806
+ }),
807
+ /*#__PURE__*/ _jsx(TableCell, {
808
+ align: "right"
809
+ })
810
+ ]
811
+ })
812
+ }),
813
+ /*#__PURE__*/ _jsx(TableBody, {
814
+ children: ((_ref8 = results == null ? void 0 : results.experiment.variants) != null ? _ref8 : []).map((variant, index)=>{
815
+ var _ref, _ref1, _ref2, _ref3, _variant_name;
816
+ const summary = summarizeVariantStats((_ref = results == null ? void 0 : results.stats[variant.id]) != null ? _ref : {});
817
+ // Lift + z-test confidence vs the first variant (AGL-265).
818
+ const control = results == null ? void 0 : results.experiment.variants[0];
819
+ const comparison = index > 0 && control ? compareVariants((_ref1 = results == null ? void 0 : results.stats[control.id]) != null ? _ref1 : {}, (_ref2 = results == null ? void 0 : results.stats[variant.id]) != null ? _ref2 : {}) : null;
820
+ const leader = summary.exposures > 0 && ((_ref3 = results == null ? void 0 : results.experiment.variants) != null ? _ref3 : []).every((other)=>{
821
+ var _ref;
822
+ const otherSummary = summarizeVariantStats((_ref = results == null ? void 0 : results.stats[other.id]) != null ? _ref : {});
823
+ return summary.rate >= otherSummary.rate;
824
+ });
825
+ return /*#__PURE__*/ _jsxs(TableRow, {
826
+ selected: leader,
827
+ children: [
828
+ /*#__PURE__*/ _jsxs(TableCell, {
829
+ children: [
830
+ (_variant_name = variant.name) != null ? _variant_name : variant.id.toUpperCase(),
831
+ (results == null ? void 0 : results.experiment.winnerVariantId) === variant.id ? ' 🏆' : leader ? ' ▲' : ''
832
+ ]
833
+ }),
834
+ /*#__PURE__*/ _jsx(TableCell, {
835
+ children: summary.exposures
836
+ }),
837
+ /*#__PURE__*/ _jsx(TableCell, {
838
+ children: summary.conversions
839
+ }),
840
+ /*#__PURE__*/ _jsx(TableCell, {
841
+ children: `${(summary.rate * 100).toFixed(1)}%`
842
+ }),
843
+ /*#__PURE__*/ _jsx(TableCell, {
844
+ children: comparison ? `${comparison.lift != null ? `${comparison.lift >= 0 ? '+' : ''}${(comparison.lift * 100).toFixed(0)}%` : '—'}${comparison.confidence != null ? ` · ${(comparison.confidence * 100).toFixed(0)}% conf.` : ' · needs data'}` : 'control'
845
+ }),
846
+ /*#__PURE__*/ _jsx(TableCell, {
847
+ align: "right",
848
+ children: (results == null ? void 0 : results.experiment.status) !== 'done' ? /*#__PURE__*/ _jsx(Button, {
849
+ size: "small",
850
+ onClick: ()=>{
851
+ if (results) {
852
+ void setStatus(results.experiment, 'done', variant.id);
853
+ setResults(null);
854
+ }
855
+ },
856
+ children: 'Pick winner'
857
+ }) : null
858
+ })
859
+ ]
860
+ }, variant.id);
861
+ })
862
+ })
863
+ ]
864
+ }),
865
+ results ? /*#__PURE__*/ _jsx(ExperimentResultZone, {
866
+ hostId: hostId,
867
+ experimentId: (_results_experiment_$id = results.experiment.$id) != null ? _results_experiment_$id : '',
868
+ test: ((_results_experiment_name = results.experiment.name) == null ? void 0 : _results_experiment_name.trim()) || ((_results_experiment_$id1 = results.experiment.$id) != null ? _results_experiment_$id1 : '')
869
+ }) : null
870
+ ]
871
+ }),
872
+ /*#__PURE__*/ _jsx(DialogActions, {
873
+ children: /*#__PURE__*/ _jsx(Button, {
874
+ color: "inherit",
875
+ onClick: ()=>setResults(null),
876
+ children: 'Close'
877
+ })
878
+ })
879
+ ]
880
+ })
881
+ ]
882
+ });
883
+ }
884
+ HostExperimentsCard.displayName = 'HostExperimentsCard';
885
+ export default HostExperimentsCard;
886
+
887
+ //# sourceMappingURL=host-experiments-card.component.js.map