@aglyn/plugins-ai 1.0.0-beta.263 → 1.0.0-beta.266

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 (109) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/beauty.webp +0 -0
  3. package/src/lib/assets/site-styles/blog.webp +0 -0
  4. package/src/lib/assets/site-styles/business.webp +0 -0
  5. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  6. package/src/lib/assets/site-styles/education.webp +0 -0
  7. package/src/lib/assets/site-styles/events.webp +0 -0
  8. package/src/lib/assets/site-styles/fitness.webp +0 -0
  9. package/src/lib/assets/site-styles/landing.webp +0 -0
  10. package/src/lib/assets/site-styles/music.webp +0 -0
  11. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  12. package/src/lib/assets/site-styles/personal.webp +0 -0
  13. package/src/lib/assets/site-styles/photography.webp +0 -0
  14. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  15. package/src/lib/assets/site-styles/professional.webp +0 -0
  16. package/src/lib/assets/site-styles/realestate.webp +0 -0
  17. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  18. package/src/lib/assets/site-styles/store.webp +0 -0
  19. package/src/lib/assets/site-styles/studio.webp +0 -0
  20. package/src/lib/assets/site-styles/trades.webp +0 -0
  21. package/src/lib/assets/site-styles/wellness.webp +0 -0
  22. package/src/lib/assets/site-styles/yoga.webp +0 -0
  23. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  24. package/src/lib/components/ai-site-start-card.component.js +256 -236
  25. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  26. package/src/lib/components/ai-site-start-steps.component.d.ts +108 -0
  27. package/src/lib/components/ai-site-start-steps.component.js +451 -0
  28. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  29. package/src/lib/components/ai-site-start-tracking.d.ts +114 -0
  30. package/src/lib/components/ai-site-start-tracking.js +277 -0
  31. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  32. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  33. package/src/lib/components/ai-site-style-previews.js +115 -0
  34. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  35. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  36. package/src/lib/insights/ai-insight-digest.js +35 -11
  37. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  39. package/src/lib/jobs/ai-job-page-language.js +20 -2
  40. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  41. package/src/lib/jobs/ai-job-page-sections.js +8 -2
  42. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  43. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  44. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  45. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-site-datasets.d.ts +93 -2
  47. package/src/lib/jobs/ai-job-site-datasets.js +308 -21
  48. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +191 -13
  50. package/src/lib/jobs/ai-layout-stock-photos.js +1709 -69
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  53. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  54. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  59. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  60. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.d.ts +23 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js +163 -0
  63. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js.map +1 -0
  64. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.d.ts +23 -0
  65. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js +53 -0
  66. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js.map +1 -0
  67. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.d.ts +21 -0
  68. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js +119 -0
  69. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js.map +1 -0
  70. package/src/lib/layout-language/ai-layout-compiler.js +54 -15
  71. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  72. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  73. package/src/lib/layout-language/ai-layout-design.js +14 -13
  74. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  75. package/src/lib/layout-language/ai-layout-listings.d.ts +55 -0
  76. package/src/lib/layout-language/ai-layout-listings.js +103 -5
  77. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  78. package/src/lib/layout-language/ai-layout-pictures.d.ts +42 -8
  79. package/src/lib/layout-language/ai-layout-pictures.js +91 -6
  80. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  81. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  82. package/src/lib/model/ai-site-home-arcs.js +741 -0
  83. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  84. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  85. package/src/lib/model/ai-site-inventory.js.map +1 -1
  86. package/src/lib/model/ai-site-job.d.ts +9 -6
  87. package/src/lib/model/ai-site-job.js +8 -5
  88. package/src/lib/model/ai-site-job.js.map +1 -1
  89. package/src/lib/model/ai-site-kinds.js +24 -24
  90. package/src/lib/model/ai-site-kinds.js.map +1 -1
  91. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  92. package/src/lib/model/ai-site-start-steps.js +127 -0
  93. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  94. package/src/lib/model/ai-site-start.js +4 -1
  95. package/src/lib/model/ai-site-start.js.map +1 -1
  96. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  97. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  98. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  99. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  100. package/src/lib/runtime/ai-doctrine.js +9 -5
  101. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  102. package/src/lib/runtime/ai-palette.generated.js +5 -2
  103. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  104. package/src/lib/runtime/site-inventory.js +6 -0
  105. package/src/lib/runtime/site-inventory.js.map +1 -1
  106. package/src/lib/server/ai-insight-digest-route.js +2 -2
  107. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  108. package/src/lib/usage/assist-free-taste.js +3 -0
  109. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -0,0 +1,451 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
20
+ import { mdiBookOpenPageVariant, mdiBriefcaseOutline, mdiCalendarHeart, mdiCreation, mdiHandHeart, mdiScaleBalance, mdiShopping, mdiSilverwareForkKnife, mdiViewGridOutline, mdiWrench } from "@aglyn/shared-data-mdi";
21
+ import { HelpTip } from "@aglyn/shared-ui-jsx";
22
+ import { OptionCardGrid } from "@aglyn/shared-ui-jsx/components/option-card-grid.component";
23
+ import { Box, Button, Divider, LinearProgress, MenuItem, Stack, Step, StepLabel, Stepper, TextField, Typography } from "@mui/material";
24
+ import { Fragment } from "react";
25
+ import { AI_SITE_FREE_PAGES_NOTE, AI_SITE_SUBMISSION_CHOICES } from "../model/ai-site-job.js";
26
+ import { AI_SITE_KINDS } from "../model/ai-site-kinds.js";
27
+ import { AI_SITE_START_TYPES, aiSiteStartKind } from "../model/ai-site-start.js";
28
+ import { AI_SITE_START_STEP_LABELS, AI_SITE_START_STEPS, aiSiteStartStepIndex } from "../model/ai-site-start-steps.js";
29
+ import { aiSiteStylePreview } from "./ai-site-style-previews.js";
30
+ /**
31
+ * The guided start's question steps (AGL-3898), one decision each. Each step
32
+ * draws its own answers and writes them through `answer`, exactly the patch
33
+ * the single form wrote, so the answers object is the same whichever way it
34
+ * was filled in. The dialog around them (`ai-site-start-card.component.tsx`)
35
+ * owns the step, Back and Next, validation and the job.
36
+ */ /** Each part of the guided start links its own section of the Start with AI guide (AGL-3660). */ export const START_STEP_HELP = {
37
+ kind: pluginDocsHelp('aiStart', {
38
+ anchor: '#your-business',
39
+ excerpt: 'What kind of site it is picks its style and pages; who it is for is who every page and the contact form are written for.'
40
+ }),
41
+ audience: pluginDocsHelp('aiStart', {
42
+ anchor: '#audience',
43
+ excerpt: 'Who the site is for is who every page, every search listing and the contact form are written for. Optional.'
44
+ }),
45
+ review: pluginDocsHelp('aiStart', {
46
+ anchor: '#review',
47
+ excerpt: 'Every answer, each with Edit to change it. Plan my site creates the site, plans it, writes the pages and publishes it.'
48
+ }),
49
+ style: pluginDocsHelp('aiSiteLooks', {
50
+ anchor: '#choose-a-style',
51
+ excerpt: 'The style decides the look — theme, colors, fonts, buttons — and which pages the site usually has. Every site gets its own variation.'
52
+ }),
53
+ details: pluginDocsHelp('aiStart', {
54
+ anchor: '#details',
55
+ excerpt: 'Where contact form messages go, how many pages to write, and on paid plans whether to draft a welcome email. All can be changed later.'
56
+ })
57
+ };
58
+ /** The icon on each kind-of-site card: the icon of the style that answer picks. */ const START_TYPE_ICONS = {
59
+ 'a local service business': mdiBriefcaseOutline.path,
60
+ 'a restaurant or cafe': mdiSilverwareForkKnife.path,
61
+ 'a professional practice': mdiScaleBalance.path,
62
+ 'a trades business': mdiWrench.path,
63
+ 'a shop': mdiShopping.path,
64
+ 'a nonprofit': mdiHandHeart.path,
65
+ 'a personal portfolio': mdiViewGridOutline.path,
66
+ 'an event': mdiCalendarHeart.path
67
+ };
68
+ /** "a restaurant or cafe" as a card title: "Restaurant or cafe". The answer itself keeps its words. */ export function startTypeTitle(type) {
69
+ const words = type.replace(/^(a|an)\s+/i, '');
70
+ return words.charAt(0).toUpperCase() + words.slice(1);
71
+ }
72
+ /** The id the style grid gives "Pick for me", which is no kind: the answers' `kind: null`. */ export const AI_SITE_STYLE_AUTO = 'auto';
73
+ /** The progress through the steps: labeled steps on a wide screen, a bar and "Step 3 of 6" on a phone. */ export function StartProgress({ step, compact }) {
74
+ const index = aiSiteStartStepIndex(step);
75
+ const count = AI_SITE_START_STEPS.length;
76
+ const name = `Step ${index + 1} of ${count}`;
77
+ if (compact) {
78
+ return /*#__PURE__*/ _jsxs(Stack, {
79
+ spacing: 1,
80
+ "data-start-area": "progress",
81
+ role: "group",
82
+ "aria-label": name,
83
+ children: [
84
+ /*#__PURE__*/ _jsx(Typography, {
85
+ variant: "caption",
86
+ color: "text.secondary",
87
+ children: `${name}: ${AI_SITE_START_STEP_LABELS[step]}`
88
+ }),
89
+ /*#__PURE__*/ _jsx(LinearProgress, {
90
+ variant: "determinate",
91
+ value: (index + 1) / count * 100,
92
+ "aria-hidden": true
93
+ })
94
+ ]
95
+ });
96
+ }
97
+ return /*#__PURE__*/ _jsx(Stepper, {
98
+ activeStep: index,
99
+ alternativeLabel: true,
100
+ "aria-label": name,
101
+ "data-start-area": "progress",
102
+ children: AI_SITE_START_STEPS.map((id)=>/*#__PURE__*/ _jsx(Step, {
103
+ children: /*#__PURE__*/ _jsx(StepLabel, {
104
+ children: AI_SITE_START_STEP_LABELS[id]
105
+ })
106
+ }, id))
107
+ });
108
+ }
109
+ /** A text answer's tracking: each change, and leaving the field, which ends one edit. */ const typed = (tracker, field)=>({
110
+ onBlur: ()=>tracker.fieldLeft(field)
111
+ });
112
+ /** Step: what kind of site it is — the one required answer — typed, or picked from cards. */ export function KindStep({ answers, answer, tracker, errors, fieldRef }) {
113
+ var _ref;
114
+ var _errors_siteType;
115
+ const picked = AI_SITE_START_TYPES.includes(answers.siteType) ? answers.siteType : null;
116
+ return /*#__PURE__*/ _jsxs(Stack, {
117
+ spacing: 3,
118
+ children: [
119
+ /*#__PURE__*/ _jsx(Box, {
120
+ "data-start-area": "field-siteType",
121
+ children: /*#__PURE__*/ _jsx(TextField, _extends({
122
+ fullWidth: true,
123
+ required: true,
124
+ inputRef: fieldRef('siteType'),
125
+ label: "What kind of site are you creating?",
126
+ placeholder: "a neighborhood dog groomer that takes bookings",
127
+ value: answers.siteType,
128
+ onChange: (event)=>{
129
+ answer({
130
+ siteType: event.target.value
131
+ });
132
+ tracker.fieldChanged('siteType');
133
+ }
134
+ }, typed(tracker, 'siteType'), {
135
+ error: Boolean(errors.siteType),
136
+ helperText: (_ref = (_errors_siteType = errors.siteType) == null ? void 0 : _errors_siteType.message) != null ? _ref : 'A few words is enough, or pick one below.'
137
+ }))
138
+ }),
139
+ /*#__PURE__*/ _jsxs(Stack, {
140
+ spacing: 1.5,
141
+ children: [
142
+ /*#__PURE__*/ _jsx(Typography, {
143
+ variant: "overline",
144
+ color: "text.secondary",
145
+ component: "h2",
146
+ children: 'Or pick the closest'
147
+ }),
148
+ /*#__PURE__*/ _jsx(OptionCardGrid, {
149
+ label: "Kind of site",
150
+ "data-start-area": "kind-cards",
151
+ columns: {
152
+ xs: 2,
153
+ sm: 2,
154
+ md: 4
155
+ },
156
+ options: AI_SITE_START_TYPES.map((type)=>{
157
+ var _START_TYPE_ICONS_type;
158
+ return {
159
+ id: type,
160
+ title: startTypeTitle(type),
161
+ icon: (_START_TYPE_ICONS_type = START_TYPE_ICONS[type]) != null ? _START_TYPE_ICONS_type : mdiBookOpenPageVariant.path
162
+ };
163
+ }),
164
+ value: picked,
165
+ onChange: (type)=>{
166
+ answer({
167
+ siteType: type
168
+ });
169
+ tracker.fieldPicked('siteType');
170
+ }
171
+ })
172
+ ]
173
+ })
174
+ ]
175
+ });
176
+ }
177
+ /** Step: who the site is for. Optional. */ export function AudienceStep({ answers, answer, tracker, errors, fieldRef }) {
178
+ var _ref;
179
+ var _errors_audience;
180
+ return /*#__PURE__*/ _jsx(Box, {
181
+ "data-start-area": "field-audience",
182
+ children: /*#__PURE__*/ _jsx(TextField, _extends({
183
+ fullWidth: true,
184
+ inputRef: fieldRef('audience'),
185
+ label: "Who is it for?",
186
+ placeholder: "local dog owners who want a regular groom booked online",
187
+ value: answers.audience,
188
+ onChange: (event)=>{
189
+ answer({
190
+ audience: event.target.value
191
+ });
192
+ tracker.fieldChanged('audience');
193
+ }
194
+ }, typed(tracker, 'audience'), {
195
+ error: Boolean(errors.audience),
196
+ helperText: (_ref = (_errors_audience = errors.audience) == null ? void 0 : _errors_audience.message) != null ? _ref : 'Optional. It narrows who the pages are written for.'
197
+ }))
198
+ });
199
+ }
200
+ /**
201
+ * Step: the style, from cards that show it. "Pick for me" leaves the kind
202
+ * unset, which is what the single form's unpicked state was: the kind the
203
+ * first answer suggests is the one used, and the card says which.
204
+ */ export function StyleStep({ answers, answer, tracker }) {
205
+ var _answers_kind;
206
+ const suggested = aiSiteStartKind(_extends({}, answers, {
207
+ kind: null
208
+ }));
209
+ return /*#__PURE__*/ _jsx(OptionCardGrid, {
210
+ label: "Style of site",
211
+ "data-start-area": "style-cards",
212
+ columns: {
213
+ xs: 2,
214
+ sm: 2,
215
+ md: 3
216
+ },
217
+ options: [
218
+ {
219
+ id: AI_SITE_STYLE_AUTO,
220
+ title: 'Pick for me',
221
+ description: `From your answer: ${suggested.label}`,
222
+ icon: mdiCreation.path
223
+ },
224
+ ...AI_SITE_KINDS.map((kind)=>{
225
+ const image = aiSiteStylePreview(kind.id);
226
+ return _extends({
227
+ id: kind.id,
228
+ title: kind.label,
229
+ description: kind.blurb
230
+ }, image ? {
231
+ image
232
+ } : {});
233
+ })
234
+ ],
235
+ value: (_answers_kind = answers.kind) != null ? _answers_kind : AI_SITE_STYLE_AUTO,
236
+ onChange: (id)=>{
237
+ answer({
238
+ kind: id === AI_SITE_STYLE_AUTO ? null : id
239
+ });
240
+ tracker.fieldPicked('kind');
241
+ }
242
+ });
243
+ }
244
+ /** Step: where leads go, how many pages, and on a paid plan the welcome email. */ export function DetailsStep({ answers, answer, tracker, errors, fieldRef, freeTaste, band }) {
245
+ var _ref, _ref1;
246
+ var _errors_submissions, _errors_pages;
247
+ return /*#__PURE__*/ _jsxs(Stack, {
248
+ spacing: 3,
249
+ children: [
250
+ /*#__PURE__*/ _jsx(Box, {
251
+ "data-start-area": "field-submissions",
252
+ children: /*#__PURE__*/ _jsx(TextField, {
253
+ select: true,
254
+ fullWidth: true,
255
+ inputRef: fieldRef('submissions'),
256
+ label: "Where do form submissions go?",
257
+ value: answers.submissions,
258
+ onChange: (event)=>{
259
+ answer({
260
+ submissions: event.target.value
261
+ });
262
+ tracker.fieldPicked('submissions');
263
+ },
264
+ error: Boolean(errors.submissions),
265
+ helperText: (_ref = (_errors_submissions = errors.submissions) == null ? void 0 : _errors_submissions.message) != null ? _ref : 'You can change this on the form itself afterwards.',
266
+ children: AI_SITE_SUBMISSION_CHOICES.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
267
+ value: option.id,
268
+ children: `${option.label} — ${option.blurb}`
269
+ }, option.id))
270
+ })
271
+ }),
272
+ /*#__PURE__*/ _jsxs(Stack, {
273
+ direction: {
274
+ xs: 'column',
275
+ sm: 'row'
276
+ },
277
+ spacing: 2,
278
+ children: [
279
+ /*#__PURE__*/ _jsx(Box, {
280
+ "data-start-area": "field-pages",
281
+ sx: {
282
+ flex: 1
283
+ },
284
+ children: /*#__PURE__*/ _jsx(TextField, {
285
+ select: true,
286
+ fullWidth: true,
287
+ inputRef: fieldRef('pages'),
288
+ label: "Pages",
289
+ value: answers.pages,
290
+ onChange: (event)=>{
291
+ answer({
292
+ pages: Number(event.target.value)
293
+ });
294
+ tracker.fieldPicked('pages');
295
+ },
296
+ error: Boolean(errors.pages),
297
+ helperText: (_ref1 = (_errors_pages = errors.pages) == null ? void 0 : _errors_pages.message) != null ? _ref1 : freeTaste ? AI_SITE_FREE_PAGES_NOTE : undefined,
298
+ children: Array.from({
299
+ length: band.max - band.min + 1
300
+ }, (_, index)=>band.min + index).map((count)=>/*#__PURE__*/ _jsx(MenuItem, {
301
+ value: count,
302
+ children: count
303
+ }, count))
304
+ })
305
+ }),
306
+ !freeTaste && /*#__PURE__*/ _jsx(Box, {
307
+ "data-start-area": "field-welcomeEmail",
308
+ sx: {
309
+ flex: 1
310
+ },
311
+ children: /*#__PURE__*/ _jsxs(TextField, {
312
+ select: true,
313
+ fullWidth: true,
314
+ label: "Welcome email",
315
+ value: answers.welcomeEmail ? 'yes' : 'no',
316
+ onChange: (event)=>{
317
+ answer({
318
+ welcomeEmail: event.target.value === 'yes'
319
+ });
320
+ tracker.fieldPicked('welcomeEmail');
321
+ },
322
+ children: [
323
+ /*#__PURE__*/ _jsx(MenuItem, {
324
+ value: "yes",
325
+ children: 'Draft one'
326
+ }),
327
+ /*#__PURE__*/ _jsx(MenuItem, {
328
+ value: "no",
329
+ children: 'No'
330
+ })
331
+ ]
332
+ })
333
+ })
334
+ ]
335
+ })
336
+ ]
337
+ });
338
+ }
339
+ /** One answer on the review, with the way back to the step that asks it. */ function ReviewRow({ label, value, step, onEdit }) {
340
+ return /*#__PURE__*/ _jsxs(Stack, {
341
+ direction: "row",
342
+ spacing: 2,
343
+ sx: {
344
+ alignItems: 'flex-start',
345
+ py: 1.5
346
+ },
347
+ children: [
348
+ /*#__PURE__*/ _jsxs(Box, {
349
+ sx: {
350
+ flex: 1,
351
+ minWidth: 0
352
+ },
353
+ children: [
354
+ /*#__PURE__*/ _jsx(Typography, {
355
+ variant: "body2",
356
+ color: "text.secondary",
357
+ component: "dt",
358
+ children: label
359
+ }),
360
+ /*#__PURE__*/ _jsx(Typography, {
361
+ variant: "body1",
362
+ component: "dd",
363
+ sx: {
364
+ m: 0,
365
+ overflowWrap: 'anywhere'
366
+ },
367
+ children: value
368
+ })
369
+ ]
370
+ }),
371
+ /*#__PURE__*/ _jsx(Button, {
372
+ size: "small",
373
+ onClick: ()=>onEdit(step),
374
+ "aria-label": `Edit ${label.toLowerCase()}`,
375
+ children: 'Edit'
376
+ })
377
+ ]
378
+ });
379
+ }
380
+ /** Step: every answer, each with Edit, before "Plan my site". */ export function ReviewStep({ answers, freeTaste, onEdit, children }) {
381
+ var _ref;
382
+ const kind = aiSiteStartKind(answers);
383
+ const submissions = AI_SITE_SUBMISSION_CHOICES.find((option)=>option.id === answers.submissions);
384
+ const rows = [
385
+ {
386
+ label: 'Kind of site',
387
+ value: answers.siteType.trim(),
388
+ step: 'kind'
389
+ },
390
+ {
391
+ label: 'Who it is for',
392
+ value: answers.audience.trim() || 'Anyone (not said)',
393
+ step: 'audience'
394
+ },
395
+ {
396
+ label: 'Style',
397
+ value: answers.kind ? kind.label : `${kind.label}, picked for you`,
398
+ step: 'style'
399
+ },
400
+ {
401
+ label: 'Form submissions',
402
+ value: (_ref = submissions == null ? void 0 : submissions.label) != null ? _ref : answers.submissions,
403
+ step: 'details'
404
+ },
405
+ {
406
+ label: 'Pages',
407
+ value: String(answers.pages),
408
+ step: 'details'
409
+ },
410
+ ...freeTaste ? [] : [
411
+ {
412
+ label: 'Welcome email',
413
+ value: answers.welcomeEmail ? 'Draft one' : 'No',
414
+ step: 'details'
415
+ }
416
+ ]
417
+ ];
418
+ return /*#__PURE__*/ _jsxs(Stack, {
419
+ spacing: 3,
420
+ children: [
421
+ /*#__PURE__*/ _jsx(Box, {
422
+ component: "dl",
423
+ "data-start-area": "review",
424
+ sx: {
425
+ m: 0
426
+ },
427
+ children: rows.map((row, index)=>/*#__PURE__*/ _jsxs(Fragment, {
428
+ children: [
429
+ index > 0 && /*#__PURE__*/ _jsx(Divider, {
430
+ component: "div"
431
+ }),
432
+ /*#__PURE__*/ _jsx(ReviewRow, _extends({}, row, {
433
+ onEdit: onEdit
434
+ }))
435
+ ]
436
+ }, row.label))
437
+ }),
438
+ children
439
+ ]
440
+ });
441
+ }
442
+ /** A step's help beside its heading. */ export function StepHelp({ step }) {
443
+ const help = step === 'choose' ? null : START_STEP_HELP[step];
444
+ return help ? /*#__PURE__*/ _jsx(HelpTip, _extends({
445
+ sx: {
446
+ float: 'right'
447
+ }
448
+ }, help)) : null;
449
+ }
450
+
451
+ //# sourceMappingURL=ai-site-start-steps.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-start-steps.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport {\n mdiBookOpenPageVariant,\n mdiBriefcaseOutline,\n mdiCalendarHeart,\n mdiCreation,\n mdiHandHeart,\n mdiScaleBalance,\n mdiShopping,\n mdiSilverwareForkKnife,\n mdiViewGridOutline,\n mdiWrench,\n} from '@aglyn/shared-data-mdi'\nimport { HelpTip } from '@aglyn/shared-ui-jsx'\nimport { OptionCardGrid } from '@aglyn/shared-ui-jsx/components/option-card-grid.component'\nimport {\n Box,\n Button,\n Divider,\n LinearProgress,\n MenuItem,\n Stack,\n Step,\n StepLabel,\n Stepper,\n TextField,\n Typography,\n} from '@mui/material'\nimport { Fragment, type ReactNode, type Ref } from 'react'\nimport { AI_SITE_FREE_PAGES_NOTE, AI_SITE_SUBMISSION_CHOICES, type AiSiteSubmissions } from '../model/ai-site-job'\nimport { AI_SITE_KINDS } from '../model/ai-site-kinds'\nimport { AI_SITE_START_TYPES, aiSiteStartKind, type AiSiteStartAnswers } from '../model/ai-site-start'\nimport {\n AI_SITE_START_STEP_LABELS,\n AI_SITE_START_STEPS,\n aiSiteStartStepIndex,\n type AiSiteStartField,\n type AiSiteStartFieldError,\n type AiSiteStartStep,\n} from '../model/ai-site-start-steps'\nimport type { AiSiteStartTracker } from './ai-site-start-tracking'\nimport { aiSiteStylePreview } from './ai-site-style-previews'\n\n/**\n * The guided start's question steps (AGL-3898), one decision each. Each step\n * draws its own answers and writes them through `answer`, exactly the patch\n * the single form wrote, so the answers object is the same whichever way it\n * was filled in. The dialog around them (`ai-site-start-card.component.tsx`)\n * owns the step, Back and Next, validation and the job.\n */\n\n/** Each part of the guided start links its own section of the Start with AI guide (AGL-3660). */\nexport const START_STEP_HELP = {\n kind: pluginDocsHelp('aiStart', {\n anchor: '#your-business',\n excerpt:\n 'What kind of site it is picks its style and pages; who it is for is who every page and the contact form are written for.',\n }),\n audience: pluginDocsHelp('aiStart', {\n anchor: '#audience',\n excerpt: 'Who the site is for is who every page, every search listing and the contact form are written for. Optional.',\n }),\n review: pluginDocsHelp('aiStart', {\n anchor: '#review',\n excerpt: 'Every answer, each with Edit to change it. Plan my site creates the site, plans it, writes the pages and publishes it.',\n }),\n style: pluginDocsHelp('aiSiteLooks', {\n anchor: '#choose-a-style',\n excerpt:\n 'The style decides the look — theme, colors, fonts, buttons — and which pages the site usually has. Every site gets its own variation.',\n }),\n details: pluginDocsHelp('aiStart', {\n anchor: '#details',\n excerpt:\n 'Where contact form messages go, how many pages to write, and on paid plans whether to draft a welcome email. All can be changed later.',\n }),\n}\n\n/** The icon on each kind-of-site card: the icon of the style that answer picks. */\nconst START_TYPE_ICONS: Readonly<Record<string, string>> = {\n 'a local service business': mdiBriefcaseOutline.path,\n 'a restaurant or cafe': mdiSilverwareForkKnife.path,\n 'a professional practice': mdiScaleBalance.path,\n 'a trades business': mdiWrench.path,\n 'a shop': mdiShopping.path,\n 'a nonprofit': mdiHandHeart.path,\n 'a personal portfolio': mdiViewGridOutline.path,\n 'an event': mdiCalendarHeart.path,\n}\n\n/** \"a restaurant or cafe\" as a card title: \"Restaurant or cafe\". The answer itself keeps its words. */\nexport function startTypeTitle(type: string): string {\n const words = type.replace(/^(a|an)\\s+/i, '')\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\n/** The id the style grid gives \"Pick for me\", which is no kind: the answers' `kind: null`. */\nexport const AI_SITE_STYLE_AUTO = 'auto'\n\n/** The progress through the steps: labeled steps on a wide screen, a bar and \"Step 3 of 6\" on a phone. */\nexport function StartProgress({ step, compact }: { step: AiSiteStartStep; compact: boolean }) {\n const index = aiSiteStartStepIndex(step)\n const count = AI_SITE_START_STEPS.length\n const name = `Step ${index + 1} of ${count}`\n if (compact) {\n return (\n <Stack spacing={1} data-start-area=\"progress\" role=\"group\" aria-label={name}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${name}: ${AI_SITE_START_STEP_LABELS[step]}`}\n </Typography>\n <LinearProgress variant=\"determinate\" value={((index + 1) / count) * 100} aria-hidden />\n </Stack>\n )\n }\n return (\n <Stepper activeStep={index} alternativeLabel aria-label={name} data-start-area=\"progress\">\n {AI_SITE_START_STEPS.map((id) => (\n <Step key={id}>\n <StepLabel>{AI_SITE_START_STEP_LABELS[id]}</StepLabel>\n </Step>\n ))}\n </Stepper>\n )\n}\n\ninterface StepProps {\n answers: AiSiteStartAnswers\n answer: (patch: Partial<AiSiteStartAnswers>) => void\n tracker: AiSiteStartTracker\n /** Each field's reason not to go forward, once the person has tried to. */\n errors: Partial<Record<AiSiteStartField, AiSiteStartFieldError>>\n /** Where each field's input is, so a refused Next can put the cursor in it. */\n fieldRef: (field: AiSiteStartField) => Ref<HTMLInputElement>\n}\n\n/** A text answer's tracking: each change, and leaving the field, which ends one edit. */\nconst typed = (tracker: AiSiteStartTracker, field: AiSiteStartField) => ({\n onBlur: () => tracker.fieldLeft(field),\n})\n\n/** Step: what kind of site it is — the one required answer — typed, or picked from cards. */\nexport function KindStep({ answers, answer, tracker, errors, fieldRef }: StepProps) {\n const picked = AI_SITE_START_TYPES.includes(answers.siteType) ? answers.siteType : null\n return (\n <Stack spacing={3}>\n <Box data-start-area=\"field-siteType\">\n <TextField\n fullWidth\n required\n inputRef={fieldRef('siteType')}\n label=\"What kind of site are you creating?\"\n placeholder=\"a neighborhood dog groomer that takes bookings\"\n value={answers.siteType}\n onChange={(event) => {\n answer({ siteType: event.target.value })\n tracker.fieldChanged('siteType')\n }}\n {...typed(tracker, 'siteType')}\n error={Boolean(errors.siteType)}\n helperText={errors.siteType?.message ?? 'A few words is enough, or pick one below.'}\n />\n </Box>\n <Stack spacing={1.5}>\n <Typography variant=\"overline\" color=\"text.secondary\" component=\"h2\">\n {'Or pick the closest'}\n </Typography>\n <OptionCardGrid\n label=\"Kind of site\"\n data-start-area=\"kind-cards\"\n columns={{ xs: 2, sm: 2, md: 4 }}\n options={AI_SITE_START_TYPES.map((type) => ({\n id: type,\n title: startTypeTitle(type),\n icon: START_TYPE_ICONS[type] ?? mdiBookOpenPageVariant.path,\n }))}\n value={picked}\n onChange={(type) => {\n answer({ siteType: type })\n tracker.fieldPicked('siteType')\n }}\n />\n </Stack>\n </Stack>\n )\n}\n\n/** Step: who the site is for. Optional. */\nexport function AudienceStep({ answers, answer, tracker, errors, fieldRef }: StepProps) {\n return (\n <Box data-start-area=\"field-audience\">\n <TextField\n fullWidth\n inputRef={fieldRef('audience')}\n label=\"Who is it for?\"\n placeholder=\"local dog owners who want a regular groom booked online\"\n value={answers.audience}\n onChange={(event) => {\n answer({ audience: event.target.value })\n tracker.fieldChanged('audience')\n }}\n {...typed(tracker, 'audience')}\n error={Boolean(errors.audience)}\n helperText={errors.audience?.message ?? 'Optional. It narrows who the pages are written for.'}\n />\n </Box>\n )\n}\n\n/**\n * Step: the style, from cards that show it. \"Pick for me\" leaves the kind\n * unset, which is what the single form's unpicked state was: the kind the\n * first answer suggests is the one used, and the card says which.\n */\nexport function StyleStep({ answers, answer, tracker }: StepProps) {\n const suggested = aiSiteStartKind({ ...answers, kind: null })\n return (\n <OptionCardGrid\n label=\"Style of site\"\n data-start-area=\"style-cards\"\n columns={{ xs: 2, sm: 2, md: 3 }}\n options={[\n {\n id: AI_SITE_STYLE_AUTO,\n title: 'Pick for me',\n description: `From your answer: ${suggested.label}`,\n icon: mdiCreation.path,\n },\n ...AI_SITE_KINDS.map((kind) => {\n const image = aiSiteStylePreview(kind.id)\n return { id: kind.id, title: kind.label, description: kind.blurb, ...(image ? { image } : {}) }\n }),\n ]}\n value={answers.kind ?? AI_SITE_STYLE_AUTO}\n onChange={(id) => {\n answer({ kind: id === AI_SITE_STYLE_AUTO ? null : id })\n tracker.fieldPicked('kind')\n }}\n />\n )\n}\n\n/** Step: where leads go, how many pages, and on a paid plan the welcome email. */\nexport function DetailsStep({\n answers,\n answer,\n tracker,\n errors,\n fieldRef,\n freeTaste,\n band,\n}: StepProps & { freeTaste: boolean; band: { min: number; max: number } }) {\n return (\n <Stack spacing={3}>\n {/*\n The one setting a new site owner has to make, asked where they are\n already answering: a contact form nobody routed files its messages\n somewhere the person has not looked.\n */}\n <Box data-start-area=\"field-submissions\">\n <TextField\n select\n fullWidth\n inputRef={fieldRef('submissions')}\n label=\"Where do form submissions go?\"\n value={answers.submissions}\n onChange={(event) => {\n answer({ submissions: event.target.value as AiSiteSubmissions })\n tracker.fieldPicked('submissions')\n }}\n error={Boolean(errors.submissions)}\n helperText={errors.submissions?.message ?? 'You can change this on the form itself afterwards.'}\n >\n {AI_SITE_SUBMISSION_CHOICES.map((option) => (\n <MenuItem key={option.id} value={option.id}>\n {`${option.label} — ${option.blurb}`}\n </MenuItem>\n ))}\n </TextField>\n </Box>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <Box data-start-area=\"field-pages\" sx={{ flex: 1 }}>\n <TextField\n select\n fullWidth\n inputRef={fieldRef('pages')}\n label=\"Pages\"\n value={answers.pages}\n onChange={(event) => {\n answer({ pages: Number(event.target.value) })\n tracker.fieldPicked('pages')\n }}\n error={Boolean(errors.pages)}\n helperText={errors.pages?.message ?? (freeTaste ? AI_SITE_FREE_PAGES_NOTE : undefined)}\n >\n {Array.from({ length: band.max - band.min + 1 }, (_, index) => band.min + index).map((count) => (\n <MenuItem key={count} value={count}>\n {count}\n </MenuItem>\n ))}\n </TextField>\n </Box>\n {!freeTaste && (\n <Box data-start-area=\"field-welcomeEmail\" sx={{ flex: 1 }}>\n <TextField\n select\n fullWidth\n label=\"Welcome email\"\n value={answers.welcomeEmail ? 'yes' : 'no'}\n onChange={(event) => {\n answer({ welcomeEmail: event.target.value === 'yes' })\n tracker.fieldPicked('welcomeEmail')\n }}\n >\n <MenuItem value=\"yes\">{'Draft one'}</MenuItem>\n <MenuItem value=\"no\">{'No'}</MenuItem>\n </TextField>\n </Box>\n )}\n </Stack>\n </Stack>\n )\n}\n\n/** One answer on the review, with the way back to the step that asks it. */\nfunction ReviewRow({ label, value, step, onEdit }: { label: string; value: ReactNode; step: AiSiteStartStep; onEdit: (step: AiSiteStartStep) => void }) {\n return (\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'flex-start', py: 1.5 }}>\n <Box sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"dt\">\n {label}\n </Typography>\n <Typography variant=\"body1\" component=\"dd\" sx={{ m: 0, overflowWrap: 'anywhere' }}>\n {value}\n </Typography>\n </Box>\n <Button size=\"small\" onClick={() => onEdit(step)} aria-label={`Edit ${label.toLowerCase()}`}>\n {'Edit'}\n </Button>\n </Stack>\n )\n}\n\n/** Step: every answer, each with Edit, before \"Plan my site\". */\nexport function ReviewStep({\n answers,\n freeTaste,\n onEdit,\n children,\n}: {\n answers: AiSiteStartAnswers\n freeTaste: boolean\n onEdit: (step: AiSiteStartStep) => void\n children?: ReactNode\n}) {\n const kind = aiSiteStartKind(answers)\n const submissions = AI_SITE_SUBMISSION_CHOICES.find((option) => option.id === answers.submissions)\n const rows: Array<{ label: string; value: ReactNode; step: AiSiteStartStep }> = [\n { label: 'Kind of site', value: answers.siteType.trim(), step: 'kind' },\n { label: 'Who it is for', value: answers.audience.trim() || 'Anyone (not said)', step: 'audience' },\n { label: 'Style', value: answers.kind ? kind.label : `${kind.label}, picked for you`, step: 'style' },\n { label: 'Form submissions', value: submissions?.label ?? answers.submissions, step: 'details' },\n { label: 'Pages', value: String(answers.pages), step: 'details' },\n ...(freeTaste ? [] : [{ label: 'Welcome email', value: answers.welcomeEmail ? 'Draft one' : 'No', step: 'details' as const }]),\n ]\n return (\n <Stack spacing={3}>\n <Box component=\"dl\" data-start-area=\"review\" sx={{ m: 0 }}>\n {rows.map((row, index) => (\n <Fragment key={row.label}>\n {index > 0 && <Divider component=\"div\" />}\n <ReviewRow {...row} onEdit={onEdit} />\n </Fragment>\n ))}\n </Box>\n {children}\n </Stack>\n )\n}\n\n/** A step's help beside its heading. */\nexport function StepHelp({ step }: { step: AiSiteStartStep }) {\n const help = step === 'choose' ? null : START_STEP_HELP[step]\n return help ? <HelpTip sx={{ float: 'right' }} {...help} /> : null\n}\n"],"names":["pluginDocsHelp","mdiBookOpenPageVariant","mdiBriefcaseOutline","mdiCalendarHeart","mdiCreation","mdiHandHeart","mdiScaleBalance","mdiShopping","mdiSilverwareForkKnife","mdiViewGridOutline","mdiWrench","HelpTip","OptionCardGrid","Box","Button","Divider","LinearProgress","MenuItem","Stack","Step","StepLabel","Stepper","TextField","Typography","Fragment","AI_SITE_FREE_PAGES_NOTE","AI_SITE_SUBMISSION_CHOICES","AI_SITE_KINDS","AI_SITE_START_TYPES","aiSiteStartKind","AI_SITE_START_STEP_LABELS","AI_SITE_START_STEPS","aiSiteStartStepIndex","aiSiteStylePreview","START_STEP_HELP","kind","anchor","excerpt","audience","review","style","details","START_TYPE_ICONS","path","startTypeTitle","type","words","replace","charAt","toUpperCase","slice","AI_SITE_STYLE_AUTO","StartProgress","step","compact","index","count","length","name","spacing","data-start-area","role","aria-label","variant","color","value","aria-hidden","activeStep","alternativeLabel","map","id","typed","tracker","field","onBlur","fieldLeft","KindStep","answers","answer","errors","fieldRef","picked","includes","siteType","fullWidth","required","inputRef","label","placeholder","onChange","event","target","fieldChanged","error","Boolean","helperText","message","component","columns","xs","sm","md","options","title","icon","fieldPicked","AudienceStep","StyleStep","suggested","description","image","blurb","DetailsStep","freeTaste","band","select","submissions","option","direction","sx","flex","pages","Number","undefined","Array","from","max","min","_","welcomeEmail","ReviewRow","onEdit","alignItems","py","minWidth","m","overflowWrap","size","onClick","toLowerCase","ReviewStep","children","find","rows","trim","String","row","StepHelp","help","float"],"mappings":"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"}
@@ -0,0 +1,114 @@
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
+ */
17
+ import { type AnalyticsEventName, type AnalyticsEventParams } from '@aglyn/aglyn/app-utils/analytics-events';
18
+ import { type AiSiteStartErrorCode, type AiSiteStartField, type AiSiteStartStep } from '../model/ai-site-start-steps';
19
+ /**
20
+ * Where people get lost in the guided start (AGL-3898), as the console's own
21
+ * product analytics: each step drawn, left forward or back and how long it
22
+ * took, the step the start was abandoned on, the field a step refused on,
23
+ * and the signs of a struggle — one field changed again and again, a step
24
+ * left idle, fast repeated clicks on one spot (rage) and clicks on parts of
25
+ * the form that do nothing (dead).
26
+ *
27
+ * Every event goes through `trackEvent`, the one door the console's GA4
28
+ * taxonomy has, so it is consent-gated exactly as every other console event
29
+ * is (`firebase-app.layout.tsx` registers the transport, or the refusal that
30
+ * drops it) and its params are sanitized there too. What is sent is only ever
31
+ * a step id, a field id, a closed error code, a closed area id, a count or a
32
+ * duration: never anything typed, picked or named.
33
+ *
34
+ * The detector is a plain object over a clock and a timer so its rules are
35
+ * tested with fake timers; {@link useAiSiteStartTracking} binds it to the
36
+ * dialog.
37
+ */
38
+ /** No input on a step for this long is a struggle. */
39
+ export declare const AI_SITE_START_IDLE_MS = 30000;
40
+ /** This many clicks on one spot… */
41
+ export declare const AI_SITE_START_RAGE_CLICKS = 3;
42
+ /** …within this long… */
43
+ export declare const AI_SITE_START_RAGE_WINDOW_MS = 700;
44
+ /** …and within this distance of the first, is rage. */
45
+ export declare const AI_SITE_START_RAGE_RADIUS_PX = 30;
46
+ /** One field changed this many times is a struggle. */
47
+ export declare const AI_SITE_START_STRUGGLE_EDITS = 3;
48
+ /** At most this many dead clicks are reported on one visit to a step. */
49
+ export declare const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3;
50
+ /** Why the start was left without a site started. */
51
+ export type AiSiteStartAbandonReason = 'skip' | 'close' | 'unload' | 'route';
52
+ type Send = <K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]) => void;
53
+ export interface AiSiteStartTrackerOptions {
54
+ send?: Send;
55
+ now?: () => number;
56
+ /** Whether the page is out of sight, when an idle step is not a struggle. */
57
+ hidden?: () => boolean;
58
+ }
59
+ /** A click as the detector reads it: where, and what it landed on. */
60
+ export interface AiSiteStartClick {
61
+ x: number;
62
+ y: number;
63
+ /** Whether it landed on something that acts on a click. */
64
+ interactive: boolean;
65
+ /** The closed id of the part of the form it landed in. */
66
+ area: string;
67
+ }
68
+ export interface AiSiteStartTracker {
69
+ /** A step was drawn. */
70
+ view(step: AiSiteStartStep): void;
71
+ /** The person went forward from the step on screen. */
72
+ complete(): void;
73
+ /** The person went back from the step on screen. */
74
+ back(): void;
75
+ /** A step refused to go forward on a field. */
76
+ fieldError(field: AiSiteStartField, code: AiSiteStartErrorCode): void;
77
+ /** A text answer changed; counted as one edit when the field is left. */
78
+ fieldChanged(field: AiSiteStartField): void;
79
+ /** A text answer was left. */
80
+ fieldLeft(field: AiSiteStartField): void;
81
+ /** A choice was picked, which is one edit. */
82
+ fieldPicked(field: AiSiteStartField): void;
83
+ /** Any input on the step, which puts off the idle signal. */
84
+ activity(): void;
85
+ /** A click anywhere in the dialog. */
86
+ click(click: AiSiteStartClick): void;
87
+ /** The start was left without a site started; reported once. */
88
+ abandon(reason: AiSiteStartAbandonReason): void;
89
+ /** A site was started (or the starter chosen): nothing after it is an abandonment. */
90
+ finish(): void;
91
+ /** Stops the idle timer. */
92
+ dispose(): void;
93
+ }
94
+ export declare function createAiSiteStartTracker(options?: AiSiteStartTrackerOptions): AiSiteStartTracker;
95
+ /** The attribute naming the part of the form an element is in (a closed id, never content). */
96
+ export declare const AI_SITE_START_AREA_ATTRIBUTE = "data-start-area";
97
+ /** A DOM click, as the detector reads it. */
98
+ export declare function aiSiteStartClickOf(event: {
99
+ clientX: number;
100
+ clientY: number;
101
+ target: EventTarget | null;
102
+ }): AiSiteStartClick;
103
+ /**
104
+ * The tracker for one open guided start: views each step as it is drawn, and
105
+ * reports the start abandoned when the tab goes away or the page changes
106
+ * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or
107
+ * already abandoned reports nothing more.
108
+ *
109
+ * The unmount report waits a tick, so a development double mount (React's
110
+ * StrictMode runs an effect's cleanup and then the effect again) is not read
111
+ * as the person leaving.
112
+ */
113
+ export declare function useAiSiteStartTracking(step: AiSiteStartStep | null, active: boolean): AiSiteStartTracker;
114
+ export {};