@aglyn/plugins-ai 1.0.0-beta.264 → 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 (98) 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-plan-step.d.ts +43 -8
  42. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  43. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-site-datasets.d.ts +82 -1
  45. package/src/lib/jobs/ai-job-site-datasets.js +276 -14
  46. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +108 -10
  48. package/src/lib/jobs/ai-layout-stock-photos.js +729 -87
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  51. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  52. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  53. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  54. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  59. package/src/lib/layout-language/ai-layout-compiler.js +49 -12
  60. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  61. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  62. package/src/lib/layout-language/ai-layout-design.js +14 -13
  63. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +48 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +91 -5
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-pictures.d.ts +15 -0
  68. package/src/lib/layout-language/ai-layout-pictures.js +6 -2
  69. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  70. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  71. package/src/lib/model/ai-site-home-arcs.js +741 -0
  72. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  73. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  74. package/src/lib/model/ai-site-inventory.js.map +1 -1
  75. package/src/lib/model/ai-site-job.d.ts +9 -6
  76. package/src/lib/model/ai-site-job.js +8 -5
  77. package/src/lib/model/ai-site-job.js.map +1 -1
  78. package/src/lib/model/ai-site-kinds.js +24 -24
  79. package/src/lib/model/ai-site-kinds.js.map +1 -1
  80. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  81. package/src/lib/model/ai-site-start-steps.js +127 -0
  82. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  83. package/src/lib/model/ai-site-start.js +4 -1
  84. package/src/lib/model/ai-site-start.js.map +1 -1
  85. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  86. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  87. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  88. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  89. package/src/lib/runtime/ai-doctrine.js +9 -5
  90. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  91. package/src/lib/runtime/ai-palette.generated.js +5 -2
  92. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  93. package/src/lib/runtime/site-inventory.js +6 -0
  94. package/src/lib/runtime/site-inventory.js.map +1 -1
  95. package/src/lib/server/ai-insight-digest-route.js +2 -2
  96. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  97. package/src/lib/usage/assist-free-taste.js +3 -0
  98. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -15,49 +15,28 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { lockdownRefusalText, parseLockdownRefusal, PLATFORM_BRAND_NAME } from "@aglyn/aglyn";
20
20
  import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
21
21
  import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
22
- import { mdiAccount, mdiBookOpenPageVariant, mdiBriefcaseOutline, mdiCalendarHeart, mdiCamera, mdiClockOutline, mdiContentCut, mdiCreation, mdiDumbbell, mdiHandHeart, mdiHeartPulse, mdiHomeCity, mdiMusic, mdiPaletteOutline, mdiRocketLaunch, mdiScaleBalance, mdiSchool, mdiShopping, mdiSilverwareForkKnife, mdiViewGridOutline, mdiWrench, mdiYoga, mdiPageLayoutHeaderFooter } from "@aglyn/shared-data-mdi";
22
+ import { mdiCreation, mdiPageLayoutHeaderFooter } from "@aglyn/shared-data-mdi";
23
23
  import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
24
24
  import { HelpTip, MdiIcon } from "@aglyn/shared-ui-jsx";
25
- import { OptionCardGrid } from "@aglyn/shared-ui-jsx/components/option-card-grid.component";
26
25
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
27
26
  import { useUser } from "@aglyn/tenant-feature-instance";
28
- import { Alert, AppBar, Box, Button, Card, CardActionArea, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, MenuItem, Stack, Step, StepLabel, Stepper, TextField, Toolbar, Typography } from "@mui/material";
29
- import { alpha } from "@mui/material/styles";
27
+ import { Alert, AppBar, Box, Button, Card, CardActionArea, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, Stack, Toolbar, Typography, useMediaQuery } from "@mui/material";
28
+ import { alpha, useTheme } from "@mui/material/styles";
30
29
  import { useRouter } from "next/navigation";
31
30
  import { useCallback, useEffect, useId, useRef, useState } from "react";
32
- import { AI_JOB_AUTO_CONFIRM_INPUT, AI_SITE_FREE_PAGES, AI_SITE_FREE_PAGES_NOTE, AI_SITE_SUBMISSION_CHOICES, aiFreeCreditsNoneLeftText, aiFreeCreditsResetLabel, aiFreeSiteCreditRange, aiFreeSitePrompt, aiSiteCreditRange, aiSitePagesBand } from "../model/ai-site-job.js";
31
+ import { AI_JOB_AUTO_CONFIRM_INPUT, AI_SITE_FREE_PAGES, aiFreeCreditsNoneLeftText, aiFreeCreditsResetLabel, aiFreeSiteCreditRange, aiFreeSitePrompt, aiSiteCreditRange, aiSitePagesBand } from "../model/ai-site-job.js";
33
32
  import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
34
33
  import { AI_CREDITS_CONFIRM_CODE, aiCreditRangeText } from "../model/ai-credit-estimate.js";
35
34
  import { AiCreditsPromptNotice } from "./ai-credits-prompt.component.js";
36
- import { AI_SITE_START_ANSWERS, AI_SITE_START_TYPES, aiSiteStartBrief, aiSiteStartKind, aiSiteStartInputs, aiSiteStartRefusal } from "../model/ai-site-start.js";
37
- import { AI_SITE_KINDS, aiSiteKindOfInputs } from "../model/ai-site-kinds.js";
38
- /** The icon each kind of site's card shows. */ const AI_SITE_KIND_ICONS = {
39
- business: mdiBriefcaseOutline.path,
40
- trades: mdiWrench.path,
41
- professional: mdiScaleBalance.path,
42
- wellness: mdiHeartPulse.path,
43
- restaurant: mdiSilverwareForkKnife.path,
44
- store: mdiShopping.path,
45
- portfolio: mdiViewGridOutline.path,
46
- studio: mdiPaletteOutline.path,
47
- photography: mdiCamera.path,
48
- blog: mdiBookOpenPageVariant.path,
49
- events: mdiCalendarHeart.path,
50
- fitness: mdiDumbbell.path,
51
- yoga: mdiYoga.path,
52
- beauty: mdiContentCut.path,
53
- realestate: mdiHomeCity.path,
54
- education: mdiSchool.path,
55
- nonprofit: mdiHandHeart.path,
56
- music: mdiMusic.path,
57
- personal: mdiAccount.path,
58
- landing: mdiRocketLaunch.path,
59
- 'coming-soon': mdiClockOutline.path
60
- };
35
+ import { AI_SITE_START_ANSWERS, aiSiteStartBrief, aiSiteStartInputs, aiSiteStartRefusal } from "../model/ai-site-start.js";
36
+ import { aiSiteStartFieldErrors, aiSiteStartFirstInvalidStep, aiSiteStartNextStep, aiSiteStartPreviousStep, aiSiteStartStepErrors, AI_SITE_START_STEP_FIELDS } from "../model/ai-site-start-steps.js";
37
+ import { aiSiteKindOfInputs } from "../model/ai-site-kinds.js";
38
+ import { aiSiteStartClickOf, useAiSiteStartTracking } from "./ai-site-start-tracking.js";
39
+ import { AudienceStep, DetailsStep, KindStep, ReviewStep, StartProgress, StepHelp, StyleStep } from "./ai-site-start-steps.component.js";
61
40
  import { AiJobFollow } from "./ai-job-follow.component.js";
62
41
  import { AiModelSelector } from "./ai-model-selector.component.js";
63
42
  import { useAiModelChoice } from "./use-ai-model-choice.js";
@@ -71,10 +50,6 @@ import { publishAiJob } from "./ai-jobs-store.js";
71
50
  */ export const AI_SITE_START_SURFACE_SX = {
72
51
  bgcolor: 'background.default'
73
52
  };
74
- /** The two steps, as the indicator under the header names them. */ const STEP_LABELS = [
75
- 'Choose',
76
- 'Describe'
77
- ];
78
53
  /** The soft round badge an icon sits in, in its accent's tint. */ function IconBadge({ accent, children }) {
79
54
  return /*#__PURE__*/ _jsx(Box, {
80
55
  "aria-hidden": true,
@@ -179,18 +154,6 @@ import { publishAiJob } from "./ai-jobs-store.js";
179
154
  anchor: '#how-do-you-want-to-start',
180
155
  excerpt: 'The starter site gives you a ready-made home page to edit. Start with AI asks a few questions, then plans, writes and publishes your pages.'
181
156
  }),
182
- business: pluginDocsHelp('aiStart', {
183
- anchor: '#your-business',
184
- 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.'
185
- }),
186
- style: pluginDocsHelp('aiSiteLooks', {
187
- anchor: '#choose-a-style',
188
- excerpt: 'The style decides the look — theme, colors, fonts, buttons — and which pages the site usually has. Every site gets its own variation.'
189
- }),
190
- details: pluginDocsHelp('aiStart', {
191
- anchor: '#details',
192
- 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.'
193
- }),
194
157
  estimate: pluginDocsHelp('aiCredits', {
195
158
  anchor: '#before-a-job-starts',
196
159
  excerpt: 'About what a start like this costs, measured on real builds, and the most it can cost. What it really costs is what each step spends, and you can watch that add up while it runs.'
@@ -217,7 +180,18 @@ function SectionLabel({ children, help }) {
217
180
  ]
218
181
  });
219
182
  }
220
- export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leave, pendingSite, initialAnswers = null }) {
183
+ /**
184
+ * The questions are one decision a step (AGL-3898): what kind of site it is
185
+ * (typed, or a card), who it is for, the style (cards that show each one,
186
+ * "Pick for me" first and picked), the details, and a review that plans the
187
+ * site (`ai-site-start-steps.component.tsx`). Back and Next keep every
188
+ * answer; Enter in a text answer is Next; each step's heading takes the focus
189
+ * when it is drawn; a step that cannot go forward says why beside the field
190
+ * and puts the cursor in it. The answers are the same object the single form
191
+ * held, and only the review turns them into the job, through the same
192
+ * `aiSiteStartBrief` and `aiSiteStartInputs`. Each step is reported as a
193
+ * funnel, with where people struggle (`ai-site-start-tracking.ts`).
194
+ */ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leave, pendingSite, initialAnswers = null }) {
221
195
  var _ref, _ref1, _aiFreeSitePrompt;
222
196
  var _aiSiteKindOfInputs, _modelChoice_options_options_find, _modelChoice_options;
223
197
  const { data: user } = useUser();
@@ -230,8 +204,21 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
230
204
  const [verdict, setVerdict] = useState('checking');
231
205
  // Read once: a start reopened on its answers stays one while it is open.
232
206
  const reopened = useRef(initialAnswers !== null);
233
- const [step, setStep] = useState(initialAnswers ? 'describe' : 'choose');
207
+ // A start reopened on its answers opens on the review, every answer there to change.
208
+ const [step, setStep] = useState(initialAnswers ? 'review' : 'choose');
234
209
  const [answers, setAnswers] = useState(initialAnswers != null ? initialAnswers : AI_SITE_START_ANSWERS);
210
+ // The steps whose reasons not to go forward are shown: the ones Next was refused on.
211
+ const [checked, setChecked] = useState(()=>new Set());
212
+ // A step opened from the review's Edit goes back to the review on Next.
213
+ const [returnTo, setReturnTo] = useState(null);
214
+ // Each step's heading takes the focus when the step changes, never on the first draw.
215
+ const headingRef = useRef(null);
216
+ const drawnStep = useRef(null);
217
+ const fieldRefs = useRef({});
218
+ const theme = useTheme();
219
+ const compact = useMediaQuery(theme.breakpoints.down('sm'), {
220
+ noSsr: true
221
+ });
235
222
  const [busy, setBusy] = useState(false);
236
223
  // The starter card's own busy state while `startBlank` runs (AGL-3594).
237
224
  const [startingStarter, setStartingStarter] = useState(false);
@@ -254,6 +241,8 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
254
241
  const pendingRef = useRef(pendingSite);
255
242
  pendingRef.current = pendingSite;
256
243
  const creating = Boolean(pendingSite) && !(pendingSite == null ? void 0 : pendingSite.created);
244
+ // The funnel (AGL-3898): each step drawn, left and abandoned, and where people struggle.
245
+ const tracker = useAiSiteStartTracking(step, verdict === 'ready');
257
246
  useEffect(()=>{
258
247
  if (!orgId || !uid) return;
259
248
  let active = true;
@@ -328,7 +317,7 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
328
317
  // and the list is otherwise read when the picker opens.
329
318
  const { load: loadModels, model: rememberedModel } = modelChoice;
330
319
  useEffect(()=>{
331
- if (!freeTaste && step === 'describe' && rememberedModel) loadModels();
320
+ if (!freeTaste && step === 'review' && rememberedModel) loadModels();
332
321
  }, [
333
322
  freeTaste,
334
323
  step,
@@ -425,6 +414,9 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
425
414
  }
426
415
  // The indicator and the launcher count it from the moment it exists.
427
416
  publishAiJob(created);
417
+ // The funnel's last step: the site is started, and nothing after is leaving it.
418
+ tracker.complete();
419
+ tracker.finish();
428
420
  // Where the next step is (AGL-3594): the site's "Building your site"
429
421
  // page for this job. The zone closes without the starter first, so the
430
422
  // setup page does not ask again behind it.
@@ -455,7 +447,8 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
455
447
  router,
456
448
  pickedModel,
457
449
  pendingSite,
458
- siteKind
450
+ siteKind,
451
+ tracker
459
452
  ]);
460
453
  // The zone's `startBlank`. Before the site exists it creates it with the
461
454
  // starter, and answers `false` when it could not — most often a name or
@@ -478,24 +471,96 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
478
471
  trackEvent('site_start_choice', {
479
472
  choice: 'starter'
480
473
  });
474
+ // The starter is a way to start, not a way to leave: no abandonment after it.
475
+ tracker.finish();
481
476
  runStartBlank();
482
477
  }, [
483
478
  pendingSite,
484
- runStartBlank
479
+ runStartBlank,
480
+ tracker
485
481
  ]);
486
482
  const chooseAi = useCallback(()=>{
487
483
  if (pendingSite && !pendingSite.created && !pendingSite.check()) return;
488
484
  trackEvent('site_start_choice', {
489
485
  choice: 'ai'
490
486
  });
491
- setStep('describe');
487
+ tracker.complete();
488
+ setStep('kind');
489
+ }, [
490
+ pendingSite,
491
+ tracker
492
+ ]);
493
+ // The heading of each step takes the focus as the step is drawn, so a
494
+ // keyboard and a screen reader start where the new question does.
495
+ useEffect(()=>{
496
+ var _headingRef_current;
497
+ if (verdict !== 'ready') return;
498
+ if (drawnStep.current !== null && drawnStep.current !== step) (_headingRef_current = headingRef.current) == null ? void 0 : _headingRef_current.focus();
499
+ drawnStep.current = step;
500
+ }, [
501
+ step,
502
+ verdict
503
+ ]);
504
+ /** Forward from a question step, when its answers may go forward. */ const next = useCallback(()=>{
505
+ if (step === 'choose' || step === 'review') return;
506
+ const errors = aiSiteStartStepErrors(step, answers, {
507
+ freeTaste
508
+ });
509
+ if (errors.length) {
510
+ var _fieldRefs_current_errors__field;
511
+ setChecked((current)=>new Set(current).add(step));
512
+ for (const error of errors)tracker.fieldError(error.field, error.code);
513
+ (_fieldRefs_current_errors__field = fieldRefs.current[errors[0].field]) == null ? void 0 : _fieldRefs_current_errors__field.focus();
514
+ return;
515
+ }
516
+ tracker.complete();
517
+ const target = returnTo != null ? returnTo : aiSiteStartNextStep(step);
518
+ setReturnTo(null);
519
+ // A review reached with an answer that cannot go forward opens on that answer instead.
520
+ if (target === 'review') {
521
+ const invalid = aiSiteStartFirstInvalidStep(answers, {
522
+ freeTaste
523
+ });
524
+ if (invalid && invalid !== 'review') {
525
+ setChecked((current)=>new Set(current).add(invalid));
526
+ setStep(invalid);
527
+ return;
528
+ }
529
+ }
530
+ if (target) setStep(target);
531
+ }, [
532
+ step,
533
+ answers,
534
+ freeTaste,
535
+ returnTo,
536
+ tracker
537
+ ]);
538
+ /** Back one step, keeping every answer. */ const back = useCallback(()=>{
539
+ const target = aiSiteStartPreviousStep(step);
540
+ if (!target) return;
541
+ tracker.back();
542
+ setReturnTo(null);
543
+ setStep(target);
492
544
  }, [
493
- pendingSite
545
+ step,
546
+ tracker
547
+ ]);
548
+ /** From the review, to the step that asks an answer, and back to the review on Next. */ const edit = useCallback((target)=>{
549
+ tracker.back();
550
+ setReturnTo('review');
551
+ setStep(target);
552
+ }, [
553
+ tracker
494
554
  ]);
495
555
  if (verdict !== 'ready') return null;
496
556
  // Before a job is started Skip is the blank site, which writes the starter
497
557
  // (AGL-3594); after, it only closes — the job builds the site.
498
558
  const exit = started ? leave != null ? leave : runStartBlank : runStartBlank;
559
+ // Skip, as the header draws it: before a site is started it is leaving the questions.
560
+ const skip = ()=>{
561
+ if (!started) tracker.abandon('skip');
562
+ exit();
563
+ };
499
564
  // Escape and the close control. Before "Create site" has made the site
500
565
  // (AGL-3761) there is nothing to give a starter to, so they only close,
501
566
  // and nothing is created; otherwise they are the same way out as Skip.
@@ -513,11 +578,16 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
513
578
  if (root && target instanceof Node && !root.contains(target)) return;
514
579
  }
515
580
  if (drafted) setConfirmingLeave(true);
516
- else dismissTo();
581
+ else closeTo();
582
+ };
583
+ // The dismissal itself, once nothing typed is in the way of it.
584
+ const closeTo = ()=>{
585
+ if (!started) tracker.abandon('close');
586
+ dismissTo();
517
587
  };
518
588
  const leaveDraft = ()=>{
519
589
  setConfirmingLeave(false);
520
- dismissTo();
590
+ closeTo();
521
591
  };
522
592
  // The chosen model's cost against Auto's, as the model list states it.
523
593
  const modelMultiplier = pickedModel && ((_modelChoice_options = modelChoice.options) == null ? void 0 : (_modelChoice_options_options_find = _modelChoice_options.options.find((option)=>option.id === pickedModel)) == null ? void 0 : _modelChoice_options_options_find.multiplier) || 1;
@@ -538,12 +608,71 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
538
608
  // Free workspaces — never the month's whole allowance as if none were spent.
539
609
  const estimateText = freeTaste ? freeCredits ? `${aiCreditRangeText(estimate)}. You have ${freeCredits.left.toLocaleString('en-US')} of your ${freeCredits.total.toLocaleString('en-US')} free AI credits left this month, until ${aiFreeCreditsResetLabel(freeCredits.resetsOn)}` : `${aiCreditRangeText(estimate)}, of the ${FREE_AI_TASTE_CREDITS_PER_MONTH} AI credits you get free each month` : `${aiCreditRangeText(estimate)}, estimated`;
540
610
  const choosing = step === 'choose' && !started;
611
+ const reviewing = step === 'review' && !started;
612
+ const asking = step !== 'choose' && !started;
613
+ // Each answer's reason not to go forward, shown on a step once Next was refused there.
614
+ const fieldErrors = aiSiteStartFieldErrors(answers, {
615
+ freeTaste
616
+ });
617
+ const shownErrors = Object.fromEntries(AI_SITE_START_STEP_FIELDS[step].filter((field)=>checked.has(step) && fieldErrors[field]).map((field)=>[
618
+ field,
619
+ fieldErrors[field]
620
+ ]));
621
+ const stepProps = {
622
+ answers,
623
+ answer,
624
+ tracker,
625
+ errors: shownErrors,
626
+ fieldRef: (field)=>(node)=>{
627
+ fieldRefs.current[field] = node;
628
+ }
629
+ };
630
+ const HEADINGS = {
631
+ choose: {
632
+ title: 'How do you want to start?',
633
+ lede: creating ? 'Name your site, then choose how it starts. You can change everything later.' : 'You can change everything later.'
634
+ },
635
+ kind: {
636
+ title: 'What are you building?',
637
+ lede: `Say it in a few words, or pick the closest. ${PLATFORM_BRAND_NAME} AI plans and writes your pages from it.`
638
+ },
639
+ audience: {
640
+ title: 'Who is it for?',
641
+ lede: 'The pages and the contact form are written for them.'
642
+ },
643
+ style: {
644
+ title: 'Choose a style',
645
+ lede: 'Each picture is an example home page in that style. Yours is written for your business, with its own colors and fonts within the style you pick.'
646
+ },
647
+ details: {
648
+ title: 'A few details',
649
+ lede: 'Where messages go, how many pages, and what else to draft. All can be changed later.'
650
+ },
651
+ review: {
652
+ title: 'Review your site',
653
+ lede: started ? `${PLATFORM_BRAND_NAME} AI is on it.` : `${PLATFORM_BRAND_NAME} AI plans and writes your pages from these answers, then publishes your site.`
654
+ }
655
+ };
656
+ const heading = HEADINGS[step];
657
+ // Enter in a text answer is Next. A select, a card or a button keeps its own Enter.
658
+ const onStepKeyDown = (event)=>{
659
+ if (event.key !== 'Enter' || event.nativeEvent.isComposing || !asking || reviewing) return;
660
+ const target = event.target;
661
+ if (target.tagName !== 'INPUT' || target.type === 'hidden') return;
662
+ event.preventDefault();
663
+ next();
664
+ };
541
665
  return /*#__PURE__*/ _jsxs(Dialog, {
542
666
  open: true,
543
667
  fullScreen: true,
544
668
  ref: dialogRef,
545
669
  onClose: dismiss,
546
670
  "aria-labelledby": TITLE_ID,
671
+ // Where people get lost (AGL-3898): any input puts off the idle signal,
672
+ // and every click is read for rage and dead clicks.
673
+ onClickCapture: (event)=>tracker.click(aiSiteStartClickOf(event)),
674
+ onKeyDownCapture: ()=>tracker.activity(),
675
+ onInputCapture: ()=>tracker.activity(),
547
676
  // The console's own page surface, in both modes. A Dialog's paper sits
548
677
  // at elevation 24 by default, and in dark mode MUI lightens an elevated
549
678
  // paper with a white overlay, which turned the whole takeover a
@@ -561,6 +690,7 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
561
690
  color: "inherit",
562
691
  elevation: 0,
563
692
  enableColorOnDark: true,
693
+ "data-start-area": "header",
564
694
  sx: _extends({}, AI_SITE_START_SURFACE_SX, {
565
695
  borderBottom: 1,
566
696
  borderColor: 'divider'
@@ -593,7 +723,7 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
593
723
  }),
594
724
  /*#__PURE__*/ _jsx(Button, {
595
725
  color: "inherit",
596
- onClick: exit,
726
+ onClick: skip,
597
727
  "aria-label": started ? 'Close' : 'Skip and start blank',
598
728
  children: started ? 'Close' : 'Skip'
599
729
  })
@@ -607,8 +737,9 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
607
737
  sm: 3
608
738
  }
609
739
  },
740
+ onKeyDown: onStepKeyDown,
610
741
  children: /*#__PURE__*/ _jsx(Container, {
611
- maxWidth: choosing ? 'md' : 'sm',
742
+ maxWidth: choosing || step === 'kind' || step === 'style' ? 'md' : 'sm',
612
743
  disableGutters: true,
613
744
  sx: {
614
745
  py: {
@@ -619,30 +750,38 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
619
750
  children: /*#__PURE__*/ _jsxs(Stack, {
620
751
  spacing: 4,
621
752
  children: [
622
- /*#__PURE__*/ _jsx(Stepper, {
623
- activeStep: choosing ? 0 : 1,
624
- "aria-label": choosing ? 'Step 1 of 2' : 'Step 2 of 2',
753
+ /*#__PURE__*/ _jsx(Box, {
625
754
  sx: {
626
- maxWidth: (theme)=>theme.spacing(40),
755
+ maxWidth: (t)=>t.spacing(80),
627
756
  width: '100%',
628
757
  mx: 'auto'
629
758
  },
630
- children: STEP_LABELS.map((label)=>/*#__PURE__*/ _jsx(Step, {
631
- children: /*#__PURE__*/ _jsx(StepLabel, {
632
- children: label
633
- })
634
- }, label))
759
+ children: /*#__PURE__*/ _jsx(StartProgress, {
760
+ step: step,
761
+ compact: compact
762
+ })
635
763
  }),
636
764
  /*#__PURE__*/ _jsxs(Stack, {
637
765
  spacing: 1,
638
766
  sx: {
639
767
  textAlign: choosing ? 'center' : 'left'
640
768
  },
769
+ "data-start-area": "heading",
641
770
  children: [
642
771
  /*#__PURE__*/ _jsx(Typography, {
772
+ ref: headingRef,
773
+ tabIndex: -1,
643
774
  variant: "h4",
644
775
  component: "h1",
645
- children: choosing ? 'How do you want to start?' : 'Tell us about your site'
776
+ sx: {
777
+ '&:focus': {
778
+ outline: 'none'
779
+ },
780
+ '&:focus-visible': {
781
+ outline: 'none'
782
+ }
783
+ },
784
+ children: heading.title
646
785
  }),
647
786
  /*#__PURE__*/ _jsxs(Typography, {
648
787
  variant: "body1",
@@ -652,18 +791,22 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
652
791
  sx: {
653
792
  float: 'right'
654
793
  }
655
- }, START_HELP.choose)) : null,
656
- choosing ? creating ? 'Name your site, then choose how it starts. You can change everything later.' : 'You can change everything later.' : `A few answers and ${PLATFORM_BRAND_NAME} AI plans and writes your pages, then publishes your site.`
794
+ }, START_HELP.choose)) : /*#__PURE__*/ _jsx(StepHelp, {
795
+ step: step
796
+ }),
797
+ heading.lede
657
798
  ]
658
799
  })
659
800
  ]
660
801
  }),
661
802
  notice && /*#__PURE__*/ _jsx(Alert, {
662
803
  severity: "info",
804
+ "data-start-area": "notice",
663
805
  children: notice
664
806
  }),
665
- step === 'describe' && !started && noneLeft && freeCredits && /*#__PURE__*/ _jsx(Alert, {
807
+ asking && noneLeft && freeCredits && /*#__PURE__*/ _jsx(Alert, {
666
808
  severity: "warning",
809
+ "data-start-area": "notice",
667
810
  action: orgSlug ? /*#__PURE__*/ _jsx(Button, {
668
811
  color: "inherit",
669
812
  size: "small",
@@ -672,19 +815,6 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
672
815
  }) : undefined,
673
816
  children: aiFreeCreditsNoneLeftText(freeCredits.resetsOn)
674
817
  }),
675
- step === 'describe' && !started && !noneLeft && prompt && /*#__PURE__*/ _jsx(AiCreditsPromptNotice, {
676
- prompt: prompt,
677
- noun: "site",
678
- orgSlug: orgSlug,
679
- busy: busy,
680
- onBuildWhatFits: ()=>void plan({
681
- creditsConfirmed: true
682
- }),
683
- onSmaller: ()=>void plan({
684
- creditsConfirmed: true,
685
- pages: 1
686
- })
687
- }),
688
818
  choosing && creating && pendingSite ? /*#__PURE__*/ _jsxs(Stack, {
689
819
  spacing: 2,
690
820
  component: "section",
@@ -697,6 +827,7 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
697
827
  ]
698
828
  }) : null,
699
829
  choosing ? /*#__PURE__*/ _jsxs(Box, {
830
+ "data-start-area": "choose-cards",
700
831
  sx: {
701
832
  display: 'grid',
702
833
  gridTemplateColumns: {
@@ -754,153 +885,33 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
754
885
  // Full screen: AI jobs opens in the panel this dialog covers.
755
886
  onOpenJobs: exit
756
887
  })
757
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
888
+ }) : /*#__PURE__*/ _jsxs(Box, {
889
+ component: "section",
758
890
  children: [
759
- /*#__PURE__*/ _jsxs(Stack, {
760
- spacing: 2,
761
- component: "section",
762
- "aria-label": "Your business",
763
- children: [
764
- /*#__PURE__*/ _jsx(SectionLabel, {
765
- help: START_HELP.business,
766
- children: 'Your business'
767
- }),
768
- /*#__PURE__*/ _jsx(TextField, {
769
- fullWidth: true,
770
- required: true,
771
- label: "What kind of site are you creating?",
772
- placeholder: "a neighborhood dog groomer that takes bookings",
773
- value: answers.siteType,
774
- onChange: (event)=>answer({
775
- siteType: event.target.value
776
- }),
777
- helperText: "A few words is enough, or pick one below."
778
- }),
779
- /*#__PURE__*/ _jsx(Box, {
780
- sx: {
781
- display: 'flex',
782
- flexWrap: 'wrap',
783
- gap: 1
784
- },
785
- children: AI_SITE_START_TYPES.map((type)=>/*#__PURE__*/ _jsx(Chip, {
786
- label: type,
787
- size: "small",
788
- color: answers.siteType === type ? 'primary' : 'default',
789
- variant: answers.siteType === type ? 'filled' : 'outlined',
790
- onClick: ()=>answer({
791
- siteType: type
792
- })
793
- }, type))
794
- }),
795
- /*#__PURE__*/ _jsx(TextField, {
796
- fullWidth: true,
797
- label: "Who is it for?",
798
- placeholder: "local dog owners who want a regular groom booked online",
799
- value: answers.audience,
800
- onChange: (event)=>answer({
801
- audience: event.target.value
802
- }),
803
- helperText: "Optional. It narrows who the pages are written for."
804
- })
805
- ]
806
- }),
807
- /*#__PURE__*/ _jsxs(Stack, {
808
- spacing: 2,
809
- component: "section",
810
- "aria-label": "Style",
811
- children: [
812
- /*#__PURE__*/ _jsx(SectionLabel, {
813
- help: START_HELP.style,
814
- children: 'Style'
815
- }),
816
- /*#__PURE__*/ _jsx(Typography, {
817
- variant: "body2",
818
- color: "text.secondary",
819
- children: answers.kind ? 'This decides how your site looks and which pages it usually has.' : 'Picked from your answer above. Choose another to change how your site looks and which pages it usually has.'
820
- }),
821
- /*#__PURE__*/ _jsx(OptionCardGrid, {
822
- label: "Style of site",
823
- options: AI_SITE_KINDS.map((kind)=>({
824
- id: kind.id,
825
- title: kind.label,
826
- description: kind.blurb,
827
- icon: AI_SITE_KIND_ICONS[kind.id]
828
- })),
829
- value: aiSiteStartKind(answers).id,
830
- onChange: (kind)=>answer({
831
- kind
832
- })
833
- })
834
- ]
835
- }),
836
- /*#__PURE__*/ _jsxs(Stack, {
837
- spacing: 2,
838
- component: "section",
839
- "aria-label": "Details",
840
- children: [
841
- /*#__PURE__*/ _jsx(SectionLabel, {
842
- help: START_HELP.details,
843
- children: 'Details'
844
- }),
845
- /*#__PURE__*/ _jsx(TextField, {
846
- select: true,
847
- fullWidth: true,
848
- label: "Where do form submissions go?",
849
- value: answers.submissions,
850
- onChange: (event)=>answer({
851
- submissions: event.target.value
852
- }),
853
- helperText: "You can change this on the form itself afterwards.",
854
- children: AI_SITE_SUBMISSION_CHOICES.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
855
- value: option.id,
856
- children: `${option.label} — ${option.blurb}`
857
- }, option.id))
858
- }),
859
- /*#__PURE__*/ _jsxs(Stack, {
860
- direction: {
861
- xs: 'column',
862
- sm: 'row'
863
- },
864
- spacing: 2,
865
- children: [
866
- /*#__PURE__*/ _jsx(TextField, {
867
- select: true,
868
- fullWidth: true,
869
- label: "Pages",
870
- value: answers.pages,
871
- onChange: (event)=>answer({
872
- pages: Number(event.target.value)
873
- }),
874
- helperText: freeTaste ? AI_SITE_FREE_PAGES_NOTE : undefined,
875
- children: Array.from({
876
- length: band.max - band.min + 1
877
- }, (_, index)=>band.min + index).map((count)=>/*#__PURE__*/ _jsx(MenuItem, {
878
- value: count,
879
- children: count
880
- }, count))
881
- }),
882
- !freeTaste && /*#__PURE__*/ _jsxs(TextField, {
883
- select: true,
884
- fullWidth: true,
885
- label: "Welcome email",
886
- value: answers.welcomeEmail ? 'yes' : 'no',
887
- onChange: (event)=>answer({
888
- welcomeEmail: event.target.value === 'yes'
889
- }),
890
- children: [
891
- /*#__PURE__*/ _jsx(MenuItem, {
892
- value: "yes",
893
- children: 'Draft one'
894
- }),
895
- /*#__PURE__*/ _jsx(MenuItem, {
896
- value: "no",
897
- children: 'No'
898
- })
899
- ]
900
- })
901
- ]
902
- })
903
- ]
891
+ step === 'kind' && /*#__PURE__*/ _jsx(KindStep, _extends({}, stepProps)),
892
+ step === 'audience' && /*#__PURE__*/ _jsx(AudienceStep, _extends({}, stepProps)),
893
+ step === 'style' && /*#__PURE__*/ _jsx(StyleStep, _extends({}, stepProps)),
894
+ step === 'details' && /*#__PURE__*/ _jsx(DetailsStep, _extends({}, stepProps, {
895
+ freeTaste: freeTaste,
896
+ band: band
897
+ })),
898
+ step === 'review' && /*#__PURE__*/ _jsx(ReviewStep, {
899
+ answers: answers,
900
+ freeTaste: freeTaste,
901
+ onEdit: edit,
902
+ children: !noneLeft && prompt && /*#__PURE__*/ _jsx(AiCreditsPromptNotice, {
903
+ prompt: prompt,
904
+ noun: "site",
905
+ orgSlug: orgSlug,
906
+ busy: busy,
907
+ onBuildWhatFits: ()=>void plan({
908
+ creditsConfirmed: true
909
+ }),
910
+ onSmaller: ()=>void plan({
911
+ creditsConfirmed: true,
912
+ pages: 1
913
+ })
914
+ })
904
915
  })
905
916
  ]
906
917
  })
@@ -908,8 +919,9 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
908
919
  })
909
920
  })
910
921
  }),
911
- step === 'describe' && !started && /* The footer stays put while the questions scroll. It wraps, because
922
+ asking && /* The footer stays put while the questions scroll. It wraps, because
912
923
  the estimate, the reason and the buttons are wider than a phone. */ /*#__PURE__*/ _jsxs(DialogActions, {
924
+ "data-start-area": "footer",
913
925
  sx: _extends({
914
926
  px: {
915
927
  xs: 2,
@@ -923,7 +935,7 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
923
935
  borderColor: 'divider'
924
936
  }, AI_SITE_START_SURFACE_SX),
925
937
  children: [
926
- /*#__PURE__*/ _jsxs(Stack, {
938
+ reviewing && /*#__PURE__*/ _jsxs(Stack, {
927
939
  direction: "row",
928
940
  spacing: 0.5,
929
941
  sx: {
@@ -942,20 +954,23 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
942
954
  }))
943
955
  ]
944
956
  }),
945
- refusal && /*#__PURE__*/ _jsx(Typography, {
957
+ reviewing && refusal && /*#__PURE__*/ _jsx(Typography, {
946
958
  variant: "body2",
947
959
  color: "text.secondary",
948
960
  children: refusal
949
961
  }),
950
- !freeTaste && /*#__PURE__*/ _jsx(AiModelSelector, {
962
+ reviewing && !freeTaste && /*#__PURE__*/ _jsx(AiModelSelector, {
951
963
  choice: modelChoice,
952
964
  disabled: busy
953
965
  }),
954
966
  /*#__PURE__*/ _jsx(Button, {
955
- onClick: ()=>setStep('choose'),
967
+ onClick: back,
968
+ sx: reviewing ? undefined : {
969
+ mr: 'auto'
970
+ },
956
971
  children: 'Back'
957
972
  }),
958
- /*#__PURE__*/ _jsx(Button, {
973
+ reviewing ? /*#__PURE__*/ _jsx(Button, {
959
974
  variant: "contained",
960
975
  size: "large",
961
976
  disabled: busy || Boolean(refusal) || Boolean(prompt) || noneLeft,
@@ -964,6 +979,11 @@ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leav
964
979
  path: mdiCreation.path
965
980
  }),
966
981
  children: busy ? 'Starting…' : 'Plan my site'
982
+ }) : /*#__PURE__*/ _jsx(Button, {
983
+ variant: "contained",
984
+ size: "large",
985
+ onClick: next,
986
+ children: returnTo === 'review' ? 'Back to review' : 'Next'
967
987
  })
968
988
  ]
969
989
  }),