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

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 (40) hide show
  1. package/package.json +18 -18
  2. package/src/lib/components/ai-job-site.d.ts +8 -1
  3. package/src/lib/components/ai-job-site.js +7 -0
  4. package/src/lib/components/ai-job-site.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +37 -0
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-start-card.component.js +25 -6
  8. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  9. package/src/lib/components/ai-site-start-steps.component.d.ts +8 -2
  10. package/src/lib/components/ai-site-start-steps.component.js +55 -26
  11. package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-tracking.d.ts +11 -3
  13. package/src/lib/components/ai-site-start-tracking.js +28 -9
  14. package/src/lib/components/ai-site-start-tracking.js.map +1 -1
  15. package/src/lib/components/assist-jobs-drawer.component.js +24 -2
  16. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  17. package/src/lib/jobs/ai-empty-site-sweep.d.ts +154 -0
  18. package/src/lib/jobs/ai-empty-site-sweep.js +346 -0
  19. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -0
  20. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -3
  21. package/src/lib/jobs/ai-jobs-beat.js +23 -6
  22. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  23. package/src/lib/jobs/ai-jobs.js +46 -7
  24. package/src/lib/jobs/ai-jobs.js.map +1 -1
  25. package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
  26. package/src/lib/jobs/ai-layout-stock-photos.js +321 -19
  27. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  28. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.d.ts +35 -0
  29. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js +379 -0
  30. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js.map +1 -0
  31. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.d.ts +31 -0
  32. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js +208 -0
  33. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js.map +1 -0
  34. package/src/lib/layout-language/ai-layout-design.js +5 -0
  35. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  36. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  37. package/src/lib/model/ai-jobs.types.js.map +1 -1
  38. package/src/lib/model/ai-site-start-handoff.d.ts +49 -0
  39. package/src/lib/model/ai-site-start-handoff.js +88 -0
  40. package/src/lib/model/ai-site-start-handoff.js.map +1 -0
@@ -1 +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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"}
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 Divider,\n LinearProgress,\n List,\n ListItemButton,\n MenuItem,\n Stack,\n Step,\n StepButton,\n StepLabel,\n Stepper,\n TextField,\n Typography,\n} from '@mui/material'\nimport { Fragment, useId, 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/**\n * The progress through the steps: labeled steps on a wide screen, a bar and\n * \"Step 3 of 6\" on a phone. On a wide screen a step already answered is a\n * button back to it (`onStep`), because a checked step reads as a link: the\n * guided start's dead clicks were people clicking it to go back.\n */\nexport function StartProgress({\n step,\n compact,\n onStep,\n}: {\n step: AiSiteStartStep\n compact: boolean\n onStep?: (step: AiSiteStartStep) => void\n}) {\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, at) => (\n <Step key={id}>\n {onStep && at < index ? (\n <StepButton onClick={() => onStep(id)} aria-label={`Back to ${AI_SITE_START_STEP_LABELS[id].toLowerCase()}`}>\n {AI_SITE_START_STEP_LABELS[id]}\n </StepButton>\n ) : (\n <StepLabel>{AI_SITE_START_STEP_LABELS[id]}</StepLabel>\n )}\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/**\n * One answer on the review, as one button back to the step that asks it. The\n * whole row is the target, not only \"Edit\" at its end: people clicked the\n * answer itself to change it, and nothing happened.\n */\nfunction ReviewRow({ label, value, step, onEdit }: { label: string; value: ReactNode; step: AiSiteStartStep; onEdit: (step: AiSiteStartStep) => void }) {\n const valueId = useId()\n return (\n <ListItemButton\n onClick={() => onEdit(step)}\n aria-label={`Edit ${label.toLowerCase()}`}\n aria-describedby={valueId}\n sx={{ alignItems: 'flex-start', gap: 2, py: 1.5, px: 1, mx: -1, borderRadius: 1 }}\n >\n <Box sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"span\" sx={{ display: 'block' }}>\n {label}\n </Typography>\n <Typography id={valueId} variant=\"body1\" component=\"span\" sx={{ display: 'block', overflowWrap: 'anywhere' }}>\n {value}\n </Typography>\n </Box>\n <Typography variant=\"button\" color=\"primary\" component=\"span\" aria-hidden sx={{ fontSize: '0.8125rem', pt: 0.25 }}>\n {'Edit'}\n </Typography>\n </ListItemButton>\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 <List disablePadding data-start-area=\"review\" aria-label=\"Your answers\">\n {rows.map((row, index) => (\n <Fragment key={row.label}>\n {index > 0 && <Divider component=\"li\" aria-hidden />}\n <li>\n <ReviewRow {...row} onEdit={onEdit} />\n </li>\n </Fragment>\n ))}\n </List>\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","Divider","LinearProgress","List","ListItemButton","MenuItem","Stack","Step","StepButton","StepLabel","Stepper","TextField","Typography","Fragment","useId","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","onStep","index","count","length","name","spacing","data-start-area","role","aria-label","variant","color","value","aria-hidden","activeStep","alternativeLabel","map","id","at","onClick","toLowerCase","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","valueId","aria-describedby","alignItems","gap","py","px","mx","borderRadius","minWidth","display","overflowWrap","fontSize","pt","ReviewStep","children","find","rows","trim","String","disablePadding","row","li","StepHelp","help","float"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA,SAASA,cAAc,QAAQ,mCAAkC;AACjE,SACEC,sBAAsB,EACtBC,mBAAmB,EACnBC,gBAAgB,EAChBC,WAAW,EACXC,YAAY,EACZC,eAAe,EACfC,WAAW,EACXC,sBAAsB,EACtBC,kBAAkB,EAClBC,SAAS,QACJ,yBAAwB;AAC/B,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,cAAc,QAAQ,6DAA4D;AAC3F,SACEC,GAAG,EACHC,OAAO,EACPC,cAAc,EACdC,IAAI,EACJC,cAAc,EACdC,QAAQ,EACRC,KAAK,EACLC,IAAI,EACJC,UAAU,EACVC,SAAS,EACTC,OAAO,EACPC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,QAAQ,EAAEC,KAAK,QAAkC,QAAO;AACjE,SAASC,uBAAuB,EAAEC,0BAA0B,QAAgC,0BAAsB;AAClH,SAASC,aAAa,QAAQ,4BAAwB;AACtD,SAASC,mBAAmB,EAAEC,eAAe,QAAiC,4BAAwB;AACtG,SACEC,yBAAyB,EACzBC,mBAAmB,EACnBC,oBAAoB,QAIf,kCAA8B;AAErC,SAASC,kBAAkB,QAAQ,8BAA0B;AAE7D;;;;;;CAMC,GAED,+FAA+F,GAC/F,OAAO,MAAMC,kBAAkB;IAC7BC,MAAMtC,eAAe,WAAW;QAC9BuC,QAAQ;QACRC,SACE;IACJ;IACAC,UAAUzC,eAAe,WAAW;QAClCuC,QAAQ;QACRC,SAAS;IACX;IACAE,QAAQ1C,eAAe,WAAW;QAChCuC,QAAQ;QACRC,SAAS;IACX;IACAG,OAAO3C,eAAe,eAAe;QACnCuC,QAAQ;QACRC,SACE;IACJ;IACAI,SAAS5C,eAAe,WAAW;QACjCuC,QAAQ;QACRC,SACE;IACJ;AACF,EAAC;AAED,iFAAiF,GACjF,MAAMK,mBAAqD;IACzD,4BAA4B3C,oBAAoB4C,IAAI;IACpD,wBAAwBtC,uBAAuBsC,IAAI;IACnD,2BAA2BxC,gBAAgBwC,IAAI;IAC/C,qBAAqBpC,UAAUoC,IAAI;IACnC,UAAUvC,YAAYuC,IAAI;IAC1B,eAAezC,aAAayC,IAAI;IAChC,wBAAwBrC,mBAAmBqC,IAAI;IAC/C,YAAY3C,iBAAiB2C,IAAI;AACnC;AAEA,qGAAqG,GACrG,OAAO,SAASC,eAAeC,IAAY;IACzC,MAAMC,QAAQD,KAAKE,OAAO,CAAC,eAAe;IAC1C,OAAOD,MAAME,MAAM,CAAC,GAAGC,WAAW,KAAKH,MAAMI,KAAK,CAAC;AACrD;AAEA,4FAA4F,GAC5F,OAAO,MAAMC,qBAAqB,OAAM;AAExC;;;;;CAKC,GACD,OAAO,SAASC,cAAc,EAC5BC,IAAI,EACJC,OAAO,EACPC,MAAM,EAKP;IACC,MAAMC,QAAQxB,qBAAqBqB;IACnC,MAAMI,QAAQ1B,oBAAoB2B,MAAM;IACxC,MAAMC,OAAO,CAAC,KAAK,EAAEH,QAAQ,EAAE,IAAI,EAAEC,OAAO;IAC5C,IAAIH,SAAS;QACX,qBACE,MAACtC;YAAM4C,SAAS;YAAGC,mBAAgB;YAAWC,MAAK;YAAQC,cAAYJ;;8BACrE,KAACrC;oBAAW0C,SAAQ;oBAAUC,OAAM;8BACjC,GAAGN,KAAK,EAAE,EAAE7B,yBAAyB,CAACuB,KAAK,EAAE;;8BAEhD,KAACzC;oBAAeoD,SAAQ;oBAAcE,OAAO,AAAEV,CAAAA,QAAQ,CAAA,IAAKC,QAAS;oBAAKU,aAAW;;;;IAG3F;IACA,qBACE,KAAC/C;QAAQgD,YAAYZ;QAAOa,gBAAgB;QAACN,cAAYJ;QAAME,mBAAgB;kBAC5E9B,oBAAoBuC,GAAG,CAAC,CAACC,IAAIC,mBAC5B,KAACvD;0BACEsC,UAAUiB,KAAKhB,sBACd,KAACtC;oBAAWuD,SAAS,IAAMlB,OAAOgB;oBAAKR,cAAY,CAAC,QAAQ,EAAEjC,yBAAyB,CAACyC,GAAG,CAACG,WAAW,IAAI;8BACxG5C,yBAAyB,CAACyC,GAAG;mCAGhC,KAACpD;8BAAWW,yBAAyB,CAACyC,GAAG;;eANlCA;;AAYnB;AAYA,uFAAuF,GACvF,MAAMI,QAAQ,CAACC,SAA6BC,QAA6B,CAAA;QACvEC,QAAQ,IAAMF,QAAQG,SAAS,CAACF;IAClC,CAAA;AAEA,2FAA2F,GAC3F,OAAO,SAASG,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEN,OAAO,EAAEO,MAAM,EAAEC,QAAQ,EAAa;;QAkB5DD;IAjBpB,MAAME,SAASzD,oBAAoB0D,QAAQ,CAACL,QAAQM,QAAQ,IAAIN,QAAQM,QAAQ,GAAG;IACnF,qBACE,MAACvE;QAAM4C,SAAS;;0BACd,KAAClD;gBAAImD,mBAAgB;0BACnB,cAAA,KAACxC;oBACCmE,SAAS;oBACTC,QAAQ;oBACRC,UAAUN,SAAS;oBACnBO,OAAM;oBACNC,aAAY;oBACZ1B,OAAOe,QAAQM,QAAQ;oBACvBM,UAAU,CAACC;wBACTZ,OAAO;4BAAEK,UAAUO,MAAMC,MAAM,CAAC7B,KAAK;wBAAC;wBACtCU,QAAQoB,YAAY,CAAC;oBACvB;mBACIrB,MAAMC,SAAS;oBACnBqB,OAAOC,QAAQf,OAAOI,QAAQ;oBAC9BY,UAAU,WAAEhB,mBAAAA,OAAOI,QAAQ,qBAAfJ,iBAAiBiB,OAAO,mBAAI;;;0BAG5C,MAACpF;gBAAM4C,SAAS;;kCACd,KAACtC;wBAAW0C,SAAQ;wBAAWC,OAAM;wBAAiBoC,WAAU;kCAC7D;;kCAEH,KAAC5F;wBACCkF,OAAM;wBACN9B,mBAAgB;wBAChByC,SAAS;4BAAEC,IAAI;4BAAGC,IAAI;4BAAGC,IAAI;wBAAE;wBAC/BC,SAAS9E,oBAAoB0C,GAAG,CAAC,CAACzB;gCAG1BH;mCAHoC;gCAC1C6B,IAAI1B;gCACJ8D,OAAO/D,eAAeC;gCACtB+D,IAAI,GAAElE,yBAAAA,gBAAgB,CAACG,KAAK,YAAtBH,yBAA0B5C,uBAAuB6C,IAAI;4BAC7D;;wBACAuB,OAAOmB;wBACPQ,UAAU,CAAChD;4BACTqC,OAAO;gCAAEK,UAAU1C;4BAAK;4BACxB+B,QAAQiC,WAAW,CAAC;wBACtB;;;;;;AAKV;AAEA,yCAAyC,GACzC,OAAO,SAASC,aAAa,EAAE7B,OAAO,EAAEC,MAAM,EAAEN,OAAO,EAAEO,MAAM,EAAEC,QAAQ,EAAa;;QAelED;IAdlB,qBACE,KAACzE;QAAImD,mBAAgB;kBACnB,cAAA,KAACxC;YACCmE,SAAS;YACTE,UAAUN,SAAS;YACnBO,OAAM;YACNC,aAAY;YACZ1B,OAAOe,QAAQ3C,QAAQ;YACvBuD,UAAU,CAACC;gBACTZ,OAAO;oBAAE5C,UAAUwD,MAAMC,MAAM,CAAC7B,KAAK;gBAAC;gBACtCU,QAAQoB,YAAY,CAAC;YACvB;WACIrB,MAAMC,SAAS;YACnBqB,OAAOC,QAAQf,OAAO7C,QAAQ;YAC9B6D,UAAU,WAAEhB,mBAAAA,OAAO7C,QAAQ,qBAAf6C,iBAAiBiB,OAAO,mBAAI;;;AAIhD;AAEA;;;;CAIC,GACD,OAAO,SAASW,UAAU,EAAE9B,OAAO,EAAEC,MAAM,EAAEN,OAAO,EAAa;QAmBpDK;IAlBX,MAAM+B,YAAYnF,gBAAgB,aAAKoD;QAAS9C,MAAM;;IACtD,qBACE,KAAC1B;QACCkF,OAAM;QACN9B,mBAAgB;QAChByC,SAAS;YAAEC,IAAI;YAAGC,IAAI;YAAGC,IAAI;QAAE;QAC/BC,SAAS;YACP;gBACEnC,IAAIpB;gBACJwD,OAAO;gBACPM,aAAa,CAAC,kBAAkB,EAAED,UAAUrB,KAAK,EAAE;gBACnDiB,MAAM3G,YAAY0C,IAAI;YACxB;eACGhB,cAAc2C,GAAG,CAAC,CAACnC;gBACpB,MAAM+E,QAAQjF,mBAAmBE,KAAKoC,EAAE;gBACxC,OAAO;oBAAEA,IAAIpC,KAAKoC,EAAE;oBAAEoC,OAAOxE,KAAKwD,KAAK;oBAAEsB,aAAa9E,KAAKgF,KAAK;mBAAMD,QAAQ;oBAAEA;gBAAM,IAAI,CAAC;YAC7F;SACD;QACDhD,KAAK,GAAEe,gBAAAA,QAAQ9C,IAAI,YAAZ8C,gBAAgB9B;QACvB0C,UAAU,CAACtB;YACTW,OAAO;gBAAE/C,MAAMoC,OAAOpB,qBAAqB,OAAOoB;YAAG;YACrDK,QAAQiC,WAAW,CAAC;QACtB;;AAGN;AAEA,gFAAgF,GAChF,OAAO,SAASO,YAAY,EAC1BnC,OAAO,EACPC,MAAM,EACNN,OAAO,EACPO,MAAM,EACNC,QAAQ,EACRiC,SAAS,EACTC,IAAI,EACmE;;QAoBnDnC,qBAsBEA;IAzCtB,qBACE,MAACnE;QAAM4C,SAAS;;0BAMd,KAAClD;gBAAImD,mBAAgB;0BACnB,cAAA,KAACxC;oBACCkG,MAAM;oBACN/B,SAAS;oBACTE,UAAUN,SAAS;oBACnBO,OAAM;oBACNzB,OAAOe,QAAQuC,WAAW;oBAC1B3B,UAAU,CAACC;wBACTZ,OAAO;4BAAEsC,aAAa1B,MAAMC,MAAM,CAAC7B,KAAK;wBAAsB;wBAC9DU,QAAQiC,WAAW,CAAC;oBACtB;oBACAZ,OAAOC,QAAQf,OAAOqC,WAAW;oBACjCrB,UAAU,WAAEhB,sBAAAA,OAAOqC,WAAW,qBAAlBrC,oBAAoBiB,OAAO,mBAAI;8BAE1C1E,2BAA2B4C,GAAG,CAAC,CAACmD,uBAC/B,KAAC1G;4BAAyBmD,OAAOuD,OAAOlD,EAAE;sCACvC,GAAGkD,OAAO9B,KAAK,CAAC,GAAG,EAAE8B,OAAON,KAAK,EAAE;2BADvBM,OAAOlD,EAAE;;;0BAM9B,MAACvD;gBAAM0G,WAAW;oBAAEnB,IAAI;oBAAUC,IAAI;gBAAM;gBAAG5C,SAAS;;kCACtD,KAAClD;wBAAImD,mBAAgB;wBAAc8D,IAAI;4BAAEC,MAAM;wBAAE;kCAC/C,cAAA,KAACvG;4BACCkG,MAAM;4BACN/B,SAAS;4BACTE,UAAUN,SAAS;4BACnBO,OAAM;4BACNzB,OAAOe,QAAQ4C,KAAK;4BACpBhC,UAAU,CAACC;gCACTZ,OAAO;oCAAE2C,OAAOC,OAAOhC,MAAMC,MAAM,CAAC7B,KAAK;gCAAE;gCAC3CU,QAAQiC,WAAW,CAAC;4BACtB;4BACAZ,OAAOC,QAAQf,OAAO0C,KAAK;4BAC3B1B,UAAU,YAAEhB,gBAAAA,OAAO0C,KAAK,qBAAZ1C,cAAciB,OAAO,oBAAKiB,YAAY5F,0BAA0BsG;sCAE3EC,MAAMC,IAAI,CAAC;gCAAEvE,QAAQ4D,KAAKY,GAAG,GAAGZ,KAAKa,GAAG,GAAG;4BAAE,GAAG,CAACC,GAAG5E,QAAU8D,KAAKa,GAAG,GAAG3E,OAAOc,GAAG,CAAC,CAACb,sBACpF,KAAC1C;oCAAqBmD,OAAOT;8CAC1BA;mCADYA;;;oBAMpB,CAAC4D,2BACA,KAAC3G;wBAAImD,mBAAgB;wBAAqB8D,IAAI;4BAAEC,MAAM;wBAAE;kCACtD,cAAA,MAACvG;4BACCkG,MAAM;4BACN/B,SAAS;4BACTG,OAAM;4BACNzB,OAAOe,QAAQoD,YAAY,GAAG,QAAQ;4BACtCxC,UAAU,CAACC;gCACTZ,OAAO;oCAAEmD,cAAcvC,MAAMC,MAAM,CAAC7B,KAAK,KAAK;gCAAM;gCACpDU,QAAQiC,WAAW,CAAC;4BACtB;;8CAEA,KAAC9F;oCAASmD,OAAM;8CAAO;;8CACvB,KAACnD;oCAASmD,OAAM;8CAAM;;;;;;;;;AAOpC;AAEA;;;;CAIC,GACD,SAASoE,UAAU,EAAE3C,KAAK,EAAEzB,KAAK,EAAEb,IAAI,EAAEkF,MAAM,EAAuG;IACpJ,MAAMC,UAAUhH;IAChB,qBACE,MAACV;QACC2D,SAAS,IAAM8D,OAAOlF;QACtBU,cAAY,CAAC,KAAK,EAAE4B,MAAMjB,WAAW,IAAI;QACzC+D,oBAAkBD;QAClBb,IAAI;YAAEe,YAAY;YAAcC,KAAK;YAAGC,IAAI;YAAKC,IAAI;YAAGC,IAAI,CAAC;YAAGC,cAAc;QAAE;;0BAEhF,MAACrI;gBAAIiH,IAAI;oBAAEC,MAAM;oBAAGoB,UAAU;gBAAE;;kCAC9B,KAAC1H;wBAAW0C,SAAQ;wBAAQC,OAAM;wBAAiBoC,WAAU;wBAAOsB,IAAI;4BAAEsB,SAAS;wBAAQ;kCACxFtD;;kCAEH,KAACrE;wBAAWiD,IAAIiE;wBAASxE,SAAQ;wBAAQqC,WAAU;wBAAOsB,IAAI;4BAAEsB,SAAS;4BAASC,cAAc;wBAAW;kCACxGhF;;;;0BAGL,KAAC5C;gBAAW0C,SAAQ;gBAASC,OAAM;gBAAUoC,WAAU;gBAAOlC,aAAW;gBAACwD,IAAI;oBAAEwB,UAAU;oBAAaC,IAAI;gBAAK;0BAC7G;;;;AAIT;AAEA,+DAA+D,GAC/D,OAAO,SAASC,WAAW,EACzBpE,OAAO,EACPoC,SAAS,EACTkB,MAAM,EACNe,QAAQ,EAMT;;IACC,MAAMnH,OAAON,gBAAgBoD;IAC7B,MAAMuC,cAAc9F,2BAA2B6H,IAAI,CAAC,CAAC9B,SAAWA,OAAOlD,EAAE,KAAKU,QAAQuC,WAAW;IACjG,MAAMgC,OAA0E;QAC9E;YAAE7D,OAAO;YAAgBzB,OAAOe,QAAQM,QAAQ,CAACkE,IAAI;YAAIpG,MAAM;QAAO;QACtE;YAAEsC,OAAO;YAAiBzB,OAAOe,QAAQ3C,QAAQ,CAACmH,IAAI,MAAM;YAAqBpG,MAAM;QAAW;QAClG;YAAEsC,OAAO;YAASzB,OAAOe,QAAQ9C,IAAI,GAAGA,KAAKwD,KAAK,GAAG,GAAGxD,KAAKwD,KAAK,CAAC,gBAAgB,CAAC;YAAEtC,MAAM;QAAQ;QACpG;YAAEsC,OAAO;YAAoBzB,KAAK,UAAEsD,+BAAAA,YAAa7B,KAAK,mBAAIV,QAAQuC,WAAW;YAAEnE,MAAM;QAAU;QAC/F;YAAEsC,OAAO;YAASzB,OAAOwF,OAAOzE,QAAQ4C,KAAK;YAAGxE,MAAM;QAAU;WAC5DgE,YAAY,EAAE,GAAG;YAAC;gBAAE1B,OAAO;gBAAiBzB,OAAOe,QAAQoD,YAAY,GAAG,cAAc;gBAAMhF,MAAM;YAAmB;SAAE;KAC9H;IACD,qBACE,MAACrC;QAAM4C,SAAS;;0BACd,KAAC/C;gBAAK8I,cAAc;gBAAC9F,mBAAgB;gBAASE,cAAW;0BACtDyF,KAAKlF,GAAG,CAAC,CAACsF,KAAKpG,sBACd,MAACjC;;4BACEiC,QAAQ,mBAAK,KAAC7C;gCAAQ0F,WAAU;gCAAKlC,aAAW;;0CACjD,KAAC0F;0CACC,cAAA,KAACvB,wBAAcsB;oCAAKrB,QAAQA;;;;uBAHjBqB,IAAIjE,KAAK;;YAQ3B2D;;;AAGP;AAEA,sCAAsC,GACtC,OAAO,SAASQ,SAAS,EAAEzG,IAAI,EAA6B;IAC1D,MAAM0G,OAAO1G,SAAS,WAAW,OAAOnB,eAAe,CAACmB,KAAK;IAC7D,OAAO0G,qBAAO,KAACvJ;QAAQmH,IAAI;YAAEqC,OAAO;QAAQ;OAAOD,SAAW;AAChE"}
@@ -21,8 +21,9 @@ import { type AiSiteStartErrorCode, type AiSiteStartField, type AiSiteStartStep
21
21
  * product analytics: each step drawn, left forward or back and how long it
22
22
  * took, the step the start was abandoned on, the field a step refused on,
23
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).
24
+ * left idle, fast repeated clicks on one spot (rage) and clicks on words, a
25
+ * picture or an icon in the form that do nothing (dead). A click on empty
26
+ * space or on a popup's backdrop is not a dead click (2026-10-18).
26
27
  *
27
28
  * Every event goes through `trackEvent`, the one door the console's GA4
28
29
  * taxonomy has, so it is consent-gated exactly as every other console event
@@ -64,6 +65,13 @@ export interface AiSiteStartClick {
64
65
  interactive: boolean;
65
66
  /** The closed id of the part of the form it landed in. */
66
67
  area: string;
68
+ /**
69
+ * Whether it landed on nothing a person could mistake for a control: empty
70
+ * space, a popup's backdrop (the click that closes a menu or a help
71
+ * popover), or anything outside the form's named parts. A blank click is
72
+ * never a dead click.
73
+ */
74
+ blank?: boolean;
67
75
  }
68
76
  export interface AiSiteStartTracker {
69
77
  /** A step was drawn. */
@@ -78,7 +86,7 @@ export interface AiSiteStartTracker {
78
86
  fieldChanged(field: AiSiteStartField): void;
79
87
  /** A text answer was left. */
80
88
  fieldLeft(field: AiSiteStartField): void;
81
- /** A choice was picked, which is one edit. */
89
+ /** A choice was picked: input, but not an edit (comparing cards is not a struggle). */
82
90
  fieldPicked(field: AiSiteStartField): void;
83
91
  /** Any input on the step, which puts off the idle signal. */
84
92
  activity(): void;
@@ -22,8 +22,9 @@ import { AI_SITE_START_STEPS, aiSiteStartStepIndex } from "../model/ai-site-star
22
22
  * product analytics: each step drawn, left forward or back and how long it
23
23
  * took, the step the start was abandoned on, the field a step refused on,
24
24
  * and the signs of a struggle — one field changed again and again, a step
25
- * left idle, fast repeated clicks on one spot (rage) and clicks on parts of
26
- * the form that do nothing (dead).
25
+ * left idle, fast repeated clicks on one spot (rage) and clicks on words, a
26
+ * picture or an icon in the form that do nothing (dead). A click on empty
27
+ * space or on a popup's backdrop is not a dead click (2026-10-18).
27
28
  *
28
29
  * Every event goes through `trackEvent`, the one door the console's GA4
29
30
  * taxonomy has, so it is consent-gated exactly as every other console event
@@ -138,8 +139,11 @@ export function createAiSiteStartTracker(options = {}) {
138
139
  if (!dirty.delete(field)) return;
139
140
  edited(field);
140
141
  },
141
- fieldPicked (field) {
142
- edited(field);
142
+ fieldPicked () {
143
+ // Picking card after card is how a person compares styles or kinds; it
144
+ // read as "one answer changed again and again" (2026-10-18) and is not.
145
+ // Only a typed answer rewritten is a struggle.
146
+ armIdle();
143
147
  },
144
148
  activity () {
145
149
  armIdle();
@@ -167,7 +171,7 @@ export function createAiSiteStartTracker(options = {}) {
167
171
  reported: false
168
172
  };
169
173
  }
170
- if (!click.interactive && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {
174
+ if (!click.interactive && !click.blank && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {
171
175
  deadClicks += 1;
172
176
  send('site_start_dead_click', {
173
177
  step,
@@ -221,16 +225,31 @@ export function createAiSiteStartTracker(options = {}) {
221
225
  '[tabindex]:not([tabindex="-1"])'
222
226
  ].join(', ');
223
227
  /** The attribute naming the part of the form an element is in (a closed id, never content). */ export const AI_SITE_START_AREA_ATTRIBUTE = 'data-start-area';
228
+ /** A popup's backdrop (a menu's, a select's, a help popover's): a click on it closes the popup. */ const BACKDROP = '.MuiBackdrop-root';
229
+ /**
230
+ * Whether an element is something a person sees and might click: its own
231
+ * words, a picture or an icon. A bare layout box (the space around and
232
+ * between things) is not.
233
+ */ function isContent(element) {
234
+ if (element.closest('img, svg, picture, video, canvas')) return true;
235
+ for (const node of Array.from(element.childNodes)){
236
+ var _node_textContent;
237
+ if (node.nodeType === 3 && ((_node_textContent = node.textContent) != null ? _node_textContent : '').trim()) return true;
238
+ }
239
+ return false;
240
+ }
224
241
  /** A DOM click, as the detector reads it. */ export function aiSiteStartClickOf(event) {
225
- var _ref;
226
- var _target_closest;
242
+ var _ref, _ref1;
227
243
  const target = event.target instanceof Element ? event.target : null;
228
- const area = (_ref = target == null ? void 0 : (_target_closest = target.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)) == null ? void 0 : _target_closest.getAttribute(AI_SITE_START_AREA_ATTRIBUTE)) != null ? _ref : 'form';
244
+ const named = (_ref = target == null ? void 0 : target.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)) != null ? _ref : null;
245
+ const area = (_ref1 = named == null ? void 0 : named.getAttribute(AI_SITE_START_AREA_ATTRIBUTE)) != null ? _ref1 : 'form';
246
+ const blank = !target || !named || Boolean(target.closest(BACKDROP)) || !isContent(target);
229
247
  return {
230
248
  x: event.clientX,
231
249
  y: event.clientY,
232
250
  interactive: Boolean(target == null ? void 0 : target.closest(INTERACTIVE)),
233
- area
251
+ area,
252
+ blank
234
253
  };
235
254
  }
236
255
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-start-tracking.ts"],"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 { trackEvent, type AnalyticsEventName, type AnalyticsEventParams } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useEffect, useRef } from 'react'\nimport {\n AI_SITE_START_STEPS,\n aiSiteStartStepIndex,\n type AiSiteStartErrorCode,\n type AiSiteStartField,\n type AiSiteStartStep,\n} from '../model/ai-site-start-steps'\n\n/**\n * Where people get lost in the guided start (AGL-3898), as the console's own\n * product analytics: each step drawn, left forward or back and how long it\n * took, the step the start was abandoned on, the field a step refused on,\n * and the signs of a struggle — one field changed again and again, a step\n * left idle, fast repeated clicks on one spot (rage) and clicks on parts of\n * the form that do nothing (dead).\n *\n * Every event goes through `trackEvent`, the one door the console's GA4\n * taxonomy has, so it is consent-gated exactly as every other console event\n * is (`firebase-app.layout.tsx` registers the transport, or the refusal that\n * drops it) and its params are sanitized there too. What is sent is only ever\n * a step id, a field id, a closed error code, a closed area id, a count or a\n * duration: never anything typed, picked or named.\n *\n * The detector is a plain object over a clock and a timer so its rules are\n * tested with fake timers; {@link useAiSiteStartTracking} binds it to the\n * dialog.\n */\n\n/** No input on a step for this long is a struggle. */\nexport const AI_SITE_START_IDLE_MS = 30_000\n/** This many clicks on one spot… */\nexport const AI_SITE_START_RAGE_CLICKS = 3\n/** …within this long… */\nexport const AI_SITE_START_RAGE_WINDOW_MS = 700\n/** …and within this distance of the first, is rage. */\nexport const AI_SITE_START_RAGE_RADIUS_PX = 30\n/** One field changed this many times is a struggle. */\nexport const AI_SITE_START_STRUGGLE_EDITS = 3\n/** At most this many dead clicks are reported on one visit to a step. */\nexport const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3\n\n/** Why the start was left without a site started. */\nexport type AiSiteStartAbandonReason = 'skip' | 'close' | 'unload' | 'route'\n\ntype Send = <K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]) => void\n\nexport interface AiSiteStartTrackerOptions {\n send?: Send\n now?: () => number\n /** Whether the page is out of sight, when an idle step is not a struggle. */\n hidden?: () => boolean\n}\n\n/** A click as the detector reads it: where, and what it landed on. */\nexport interface AiSiteStartClick {\n x: number\n y: number\n /** Whether it landed on something that acts on a click. */\n interactive: boolean\n /** The closed id of the part of the form it landed in. */\n area: string\n}\n\nexport interface AiSiteStartTracker {\n /** A step was drawn. */\n view(step: AiSiteStartStep): void\n /** The person went forward from the step on screen. */\n complete(): void\n /** The person went back from the step on screen. */\n back(): void\n /** A step refused to go forward on a field. */\n fieldError(field: AiSiteStartField, code: AiSiteStartErrorCode): void\n /** A text answer changed; counted as one edit when the field is left. */\n fieldChanged(field: AiSiteStartField): void\n /** A text answer was left. */\n fieldLeft(field: AiSiteStartField): void\n /** A choice was picked, which is one edit. */\n fieldPicked(field: AiSiteStartField): void\n /** Any input on the step, which puts off the idle signal. */\n activity(): void\n /** A click anywhere in the dialog. */\n click(click: AiSiteStartClick): void\n /** The start was left without a site started; reported once. */\n abandon(reason: AiSiteStartAbandonReason): void\n /** A site was started (or the starter chosen): nothing after it is an abandonment. */\n finish(): void\n /** Stops the idle timer. */\n dispose(): void\n}\n\nexport function createAiSiteStartTracker(options: AiSiteStartTrackerOptions = {}): AiSiteStartTracker {\n const send: Send = options.send ?? ((name, params) => trackEvent(name, params))\n const now = options.now ?? (() => Date.now())\n const hidden = options.hidden ?? (() => typeof document !== 'undefined' && document.visibilityState === 'hidden')\n const opened = now()\n let step: AiSiteStartStep | null = null\n let enteredAt = opened\n let done = false\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n let idleReported = false\n let deadClicks = 0\n let burst: { x: number; y: number; at: number; count: number; reported: boolean } | null = null\n const edits = new Map<AiSiteStartField, number>()\n const dirty = new Set<AiSiteStartField>()\n const struggled = new Set<AiSiteStartField>()\n\n const index = () => (step ? aiSiteStartStepIndex(step) : 0)\n const spent = () => Math.max(0, Math.round(now() - enteredAt))\n const stopIdle = () => {\n if (idleTimer) clearTimeout(idleTimer)\n idleTimer = null\n }\n const armIdle = () => {\n stopIdle()\n if (done || !step || idleReported) return\n idleTimer = setTimeout(() => {\n idleTimer = null\n if (done || !step || idleReported || hidden()) return\n idleReported = true\n send('site_start_struggle', { step, signal: 'idle', duration_ms: AI_SITE_START_IDLE_MS })\n }, AI_SITE_START_IDLE_MS)\n }\n const edited = (field: AiSiteStartField) => {\n if (!step) return\n const count = (edits.get(field) ?? 0) + 1\n edits.set(field, count)\n if (count >= AI_SITE_START_STRUGGLE_EDITS && !struggled.has(field)) {\n struggled.add(field)\n send('site_start_struggle', { step, signal: 'edits', field, count })\n }\n }\n\n return {\n view(next) {\n if (done) return\n step = next\n enteredAt = now()\n idleReported = false\n deadClicks = 0\n burst = null\n send('site_start_step_viewed', { step: next, step_index: aiSiteStartStepIndex(next), step_count: AI_SITE_START_STEPS.length })\n armIdle()\n },\n complete() {\n if (done || !step) return\n send('site_start_step_completed', { step, step_index: index(), duration_ms: spent() })\n },\n back() {\n if (done || !step) return\n send('site_start_step_back', { step, step_index: index(), duration_ms: spent() })\n },\n fieldError(field, code) {\n if (done || !step) return\n send('site_start_field_error', { step, field, error: code })\n },\n fieldChanged(field) {\n dirty.add(field)\n },\n fieldLeft(field) {\n if (!dirty.delete(field)) return\n edited(field)\n },\n fieldPicked(field) {\n edited(field)\n },\n activity() {\n armIdle()\n },\n click(click) {\n if (done || !step) return\n const at = now()\n const near =\n burst &&\n at - burst.at <= AI_SITE_START_RAGE_WINDOW_MS &&\n Math.hypot(click.x - burst.x, click.y - burst.y) <= AI_SITE_START_RAGE_RADIUS_PX\n if (near && burst) {\n burst.count += 1\n if (burst.count >= AI_SITE_START_RAGE_CLICKS && !burst.reported) {\n burst.reported = true\n send('site_start_rage_click', { step, area: click.area, clicks: burst.count })\n }\n } else {\n burst = { x: click.x, y: click.y, at, count: 1, reported: false }\n }\n if (!click.interactive && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {\n deadClicks += 1\n send('site_start_dead_click', { step, area: click.area })\n }\n armIdle()\n },\n abandon(reason) {\n if (done) return\n done = true\n stopIdle()\n const at = step ?? AI_SITE_START_STEPS[0]\n send('site_start_abandoned', {\n step: at,\n step_index: aiSiteStartStepIndex(at),\n reason,\n duration_ms: spent(),\n total_ms: Math.max(0, Math.round(now() - opened)),\n })\n },\n finish() {\n done = true\n stopIdle()\n },\n dispose() {\n stopIdle()\n },\n }\n}\n\n/** What clicking acts on: a click on anything else in the form is a dead click. */\nconst INTERACTIVE = [\n 'button',\n 'a[href]',\n 'input',\n 'select',\n 'textarea',\n 'label',\n 'summary',\n '[role=\"button\"]',\n '[role=\"radio\"]',\n '[role=\"checkbox\"]',\n '[role=\"switch\"]',\n '[role=\"option\"]',\n '[role=\"tab\"]',\n '[role=\"link\"]',\n '[role=\"combobox\"]',\n '[role=\"menuitem\"]',\n '[role=\"slider\"]',\n '[contenteditable=\"true\"]',\n '.MuiInputBase-root',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\n/** The attribute naming the part of the form an element is in (a closed id, never content). */\nexport const AI_SITE_START_AREA_ATTRIBUTE = 'data-start-area'\n\n/** A DOM click, as the detector reads it. */\nexport function aiSiteStartClickOf(event: { clientX: number; clientY: number; target: EventTarget | null }): AiSiteStartClick {\n const target = event.target instanceof Element ? event.target : null\n const area = target?.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)?.getAttribute(AI_SITE_START_AREA_ATTRIBUTE) ?? 'form'\n return { x: event.clientX, y: event.clientY, interactive: Boolean(target?.closest(INTERACTIVE)), area }\n}\n\n/**\n * The tracker for one open guided start: views each step as it is drawn, and\n * reports the start abandoned when the tab goes away or the page changes\n * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or\n * already abandoned reports nothing more.\n *\n * The unmount report waits a tick, so a development double mount (React's\n * StrictMode runs an effect's cleanup and then the effect again) is not read\n * as the person leaving.\n */\nexport function useAiSiteStartTracking(step: AiSiteStartStep | null, active: boolean): AiSiteStartTracker {\n const ref = useRef<AiSiteStartTracker | null>(null)\n if (!ref.current) ref.current = createAiSiteStartTracker()\n const tracker = ref.current\n const leaving = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n useEffect(() => {\n if (active && step) tracker.view(step)\n }, [active, step, tracker])\n\n useEffect(() => {\n if (!active) return\n if (leaving.current) clearTimeout(leaving.current)\n leaving.current = null\n const onPageHide = () => tracker.abandon('unload')\n window.addEventListener('pagehide', onPageHide)\n return () => {\n window.removeEventListener('pagehide', onPageHide)\n leaving.current = setTimeout(() => {\n tracker.abandon('route')\n tracker.dispose()\n }, 0)\n }\n }, [active, tracker])\n\n return tracker\n}\n"],"names":["trackEvent","useEffect","useRef","AI_SITE_START_STEPS","aiSiteStartStepIndex","AI_SITE_START_IDLE_MS","AI_SITE_START_RAGE_CLICKS","AI_SITE_START_RAGE_WINDOW_MS","AI_SITE_START_RAGE_RADIUS_PX","AI_SITE_START_STRUGGLE_EDITS","AI_SITE_START_DEAD_CLICKS_PER_STEP","createAiSiteStartTracker","options","send","name","params","now","Date","hidden","document","visibilityState","opened","step","enteredAt","done","idleTimer","idleReported","deadClicks","burst","edits","Map","dirty","Set","struggled","index","spent","Math","max","round","stopIdle","clearTimeout","armIdle","setTimeout","signal","duration_ms","edited","field","count","get","set","has","add","view","next","step_index","step_count","length","complete","back","fieldError","code","error","fieldChanged","fieldLeft","delete","fieldPicked","activity","click","at","near","hypot","x","y","reported","area","clicks","interactive","abandon","reason","total_ms","finish","dispose","INTERACTIVE","join","AI_SITE_START_AREA_ATTRIBUTE","aiSiteStartClickOf","event","target","Element","closest","getAttribute","clientX","clientY","Boolean","useAiSiteStartTracking","active","ref","current","tracker","leaving","onPageHide","window","addEventListener","removeEventListener"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-start-tracking.ts"],"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 { trackEvent, type AnalyticsEventName, type AnalyticsEventParams } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useEffect, useRef } from 'react'\nimport {\n AI_SITE_START_STEPS,\n aiSiteStartStepIndex,\n type AiSiteStartErrorCode,\n type AiSiteStartField,\n type AiSiteStartStep,\n} from '../model/ai-site-start-steps'\n\n/**\n * Where people get lost in the guided start (AGL-3898), as the console's own\n * product analytics: each step drawn, left forward or back and how long it\n * took, the step the start was abandoned on, the field a step refused on,\n * and the signs of a struggle — one field changed again and again, a step\n * left idle, fast repeated clicks on one spot (rage) and clicks on words, a\n * picture or an icon in the form that do nothing (dead). A click on empty\n * space or on a popup's backdrop is not a dead click (2026-10-18).\n *\n * Every event goes through `trackEvent`, the one door the console's GA4\n * taxonomy has, so it is consent-gated exactly as every other console event\n * is (`firebase-app.layout.tsx` registers the transport, or the refusal that\n * drops it) and its params are sanitized there too. What is sent is only ever\n * a step id, a field id, a closed error code, a closed area id, a count or a\n * duration: never anything typed, picked or named.\n *\n * The detector is a plain object over a clock and a timer so its rules are\n * tested with fake timers; {@link useAiSiteStartTracking} binds it to the\n * dialog.\n */\n\n/** No input on a step for this long is a struggle. */\nexport const AI_SITE_START_IDLE_MS = 30_000\n/** This many clicks on one spot… */\nexport const AI_SITE_START_RAGE_CLICKS = 3\n/** …within this long… */\nexport const AI_SITE_START_RAGE_WINDOW_MS = 700\n/** …and within this distance of the first, is rage. */\nexport const AI_SITE_START_RAGE_RADIUS_PX = 30\n/** One field changed this many times is a struggle. */\nexport const AI_SITE_START_STRUGGLE_EDITS = 3\n/** At most this many dead clicks are reported on one visit to a step. */\nexport const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3\n\n/** Why the start was left without a site started. */\nexport type AiSiteStartAbandonReason = 'skip' | 'close' | 'unload' | 'route'\n\ntype Send = <K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]) => void\n\nexport interface AiSiteStartTrackerOptions {\n send?: Send\n now?: () => number\n /** Whether the page is out of sight, when an idle step is not a struggle. */\n hidden?: () => boolean\n}\n\n/** A click as the detector reads it: where, and what it landed on. */\nexport interface AiSiteStartClick {\n x: number\n y: number\n /** Whether it landed on something that acts on a click. */\n interactive: boolean\n /** The closed id of the part of the form it landed in. */\n area: string\n /**\n * Whether it landed on nothing a person could mistake for a control: empty\n * space, a popup's backdrop (the click that closes a menu or a help\n * popover), or anything outside the form's named parts. A blank click is\n * never a dead click.\n */\n blank?: boolean\n}\n\nexport interface AiSiteStartTracker {\n /** A step was drawn. */\n view(step: AiSiteStartStep): void\n /** The person went forward from the step on screen. */\n complete(): void\n /** The person went back from the step on screen. */\n back(): void\n /** A step refused to go forward on a field. */\n fieldError(field: AiSiteStartField, code: AiSiteStartErrorCode): void\n /** A text answer changed; counted as one edit when the field is left. */\n fieldChanged(field: AiSiteStartField): void\n /** A text answer was left. */\n fieldLeft(field: AiSiteStartField): void\n /** A choice was picked: input, but not an edit (comparing cards is not a struggle). */\n fieldPicked(field: AiSiteStartField): void\n /** Any input on the step, which puts off the idle signal. */\n activity(): void\n /** A click anywhere in the dialog. */\n click(click: AiSiteStartClick): void\n /** The start was left without a site started; reported once. */\n abandon(reason: AiSiteStartAbandonReason): void\n /** A site was started (or the starter chosen): nothing after it is an abandonment. */\n finish(): void\n /** Stops the idle timer. */\n dispose(): void\n}\n\nexport function createAiSiteStartTracker(options: AiSiteStartTrackerOptions = {}): AiSiteStartTracker {\n const send: Send = options.send ?? ((name, params) => trackEvent(name, params))\n const now = options.now ?? (() => Date.now())\n const hidden = options.hidden ?? (() => typeof document !== 'undefined' && document.visibilityState === 'hidden')\n const opened = now()\n let step: AiSiteStartStep | null = null\n let enteredAt = opened\n let done = false\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n let idleReported = false\n let deadClicks = 0\n let burst: { x: number; y: number; at: number; count: number; reported: boolean } | null = null\n const edits = new Map<AiSiteStartField, number>()\n const dirty = new Set<AiSiteStartField>()\n const struggled = new Set<AiSiteStartField>()\n\n const index = () => (step ? aiSiteStartStepIndex(step) : 0)\n const spent = () => Math.max(0, Math.round(now() - enteredAt))\n const stopIdle = () => {\n if (idleTimer) clearTimeout(idleTimer)\n idleTimer = null\n }\n const armIdle = () => {\n stopIdle()\n if (done || !step || idleReported) return\n idleTimer = setTimeout(() => {\n idleTimer = null\n if (done || !step || idleReported || hidden()) return\n idleReported = true\n send('site_start_struggle', { step, signal: 'idle', duration_ms: AI_SITE_START_IDLE_MS })\n }, AI_SITE_START_IDLE_MS)\n }\n const edited = (field: AiSiteStartField) => {\n if (!step) return\n const count = (edits.get(field) ?? 0) + 1\n edits.set(field, count)\n if (count >= AI_SITE_START_STRUGGLE_EDITS && !struggled.has(field)) {\n struggled.add(field)\n send('site_start_struggle', { step, signal: 'edits', field, count })\n }\n }\n\n return {\n view(next) {\n if (done) return\n step = next\n enteredAt = now()\n idleReported = false\n deadClicks = 0\n burst = null\n send('site_start_step_viewed', { step: next, step_index: aiSiteStartStepIndex(next), step_count: AI_SITE_START_STEPS.length })\n armIdle()\n },\n complete() {\n if (done || !step) return\n send('site_start_step_completed', { step, step_index: index(), duration_ms: spent() })\n },\n back() {\n if (done || !step) return\n send('site_start_step_back', { step, step_index: index(), duration_ms: spent() })\n },\n fieldError(field, code) {\n if (done || !step) return\n send('site_start_field_error', { step, field, error: code })\n },\n fieldChanged(field) {\n dirty.add(field)\n },\n fieldLeft(field) {\n if (!dirty.delete(field)) return\n edited(field)\n },\n fieldPicked() {\n // Picking card after card is how a person compares styles or kinds; it\n // read as \"one answer changed again and again\" (2026-10-18) and is not.\n // Only a typed answer rewritten is a struggle.\n armIdle()\n },\n activity() {\n armIdle()\n },\n click(click) {\n if (done || !step) return\n const at = now()\n const near =\n burst &&\n at - burst.at <= AI_SITE_START_RAGE_WINDOW_MS &&\n Math.hypot(click.x - burst.x, click.y - burst.y) <= AI_SITE_START_RAGE_RADIUS_PX\n if (near && burst) {\n burst.count += 1\n if (burst.count >= AI_SITE_START_RAGE_CLICKS && !burst.reported) {\n burst.reported = true\n send('site_start_rage_click', { step, area: click.area, clicks: burst.count })\n }\n } else {\n burst = { x: click.x, y: click.y, at, count: 1, reported: false }\n }\n if (!click.interactive && !click.blank && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {\n deadClicks += 1\n send('site_start_dead_click', { step, area: click.area })\n }\n armIdle()\n },\n abandon(reason) {\n if (done) return\n done = true\n stopIdle()\n const at = step ?? AI_SITE_START_STEPS[0]\n send('site_start_abandoned', {\n step: at,\n step_index: aiSiteStartStepIndex(at),\n reason,\n duration_ms: spent(),\n total_ms: Math.max(0, Math.round(now() - opened)),\n })\n },\n finish() {\n done = true\n stopIdle()\n },\n dispose() {\n stopIdle()\n },\n }\n}\n\n/** What clicking acts on: a click on anything else in the form is a dead click. */\nconst INTERACTIVE = [\n 'button',\n 'a[href]',\n 'input',\n 'select',\n 'textarea',\n 'label',\n 'summary',\n '[role=\"button\"]',\n '[role=\"radio\"]',\n '[role=\"checkbox\"]',\n '[role=\"switch\"]',\n '[role=\"option\"]',\n '[role=\"tab\"]',\n '[role=\"link\"]',\n '[role=\"combobox\"]',\n '[role=\"menuitem\"]',\n '[role=\"slider\"]',\n '[contenteditable=\"true\"]',\n '.MuiInputBase-root',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\n/** The attribute naming the part of the form an element is in (a closed id, never content). */\nexport const AI_SITE_START_AREA_ATTRIBUTE = 'data-start-area'\n\n/** A popup's backdrop (a menu's, a select's, a help popover's): a click on it closes the popup. */\nconst BACKDROP = '.MuiBackdrop-root'\n\n/**\n * Whether an element is something a person sees and might click: its own\n * words, a picture or an icon. A bare layout box (the space around and\n * between things) is not.\n */\nfunction isContent(element: Element): boolean {\n if (element.closest('img, svg, picture, video, canvas')) return true\n for (const node of Array.from(element.childNodes)) {\n if (node.nodeType === 3 && (node.textContent ?? '').trim()) return true\n }\n return false\n}\n\n/** A DOM click, as the detector reads it. */\nexport function aiSiteStartClickOf(event: { clientX: number; clientY: number; target: EventTarget | null }): AiSiteStartClick {\n const target = event.target instanceof Element ? event.target : null\n const named = target?.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`) ?? null\n const area = named?.getAttribute(AI_SITE_START_AREA_ATTRIBUTE) ?? 'form'\n const blank = !target || !named || Boolean(target.closest(BACKDROP)) || !isContent(target)\n return { x: event.clientX, y: event.clientY, interactive: Boolean(target?.closest(INTERACTIVE)), area, blank }\n}\n\n/**\n * The tracker for one open guided start: views each step as it is drawn, and\n * reports the start abandoned when the tab goes away or the page changes\n * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or\n * already abandoned reports nothing more.\n *\n * The unmount report waits a tick, so a development double mount (React's\n * StrictMode runs an effect's cleanup and then the effect again) is not read\n * as the person leaving.\n */\nexport function useAiSiteStartTracking(step: AiSiteStartStep | null, active: boolean): AiSiteStartTracker {\n const ref = useRef<AiSiteStartTracker | null>(null)\n if (!ref.current) ref.current = createAiSiteStartTracker()\n const tracker = ref.current\n const leaving = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n useEffect(() => {\n if (active && step) tracker.view(step)\n }, [active, step, tracker])\n\n useEffect(() => {\n if (!active) return\n if (leaving.current) clearTimeout(leaving.current)\n leaving.current = null\n const onPageHide = () => tracker.abandon('unload')\n window.addEventListener('pagehide', onPageHide)\n return () => {\n window.removeEventListener('pagehide', onPageHide)\n leaving.current = setTimeout(() => {\n tracker.abandon('route')\n tracker.dispose()\n }, 0)\n }\n }, [active, tracker])\n\n return tracker\n}\n"],"names":["trackEvent","useEffect","useRef","AI_SITE_START_STEPS","aiSiteStartStepIndex","AI_SITE_START_IDLE_MS","AI_SITE_START_RAGE_CLICKS","AI_SITE_START_RAGE_WINDOW_MS","AI_SITE_START_RAGE_RADIUS_PX","AI_SITE_START_STRUGGLE_EDITS","AI_SITE_START_DEAD_CLICKS_PER_STEP","createAiSiteStartTracker","options","send","name","params","now","Date","hidden","document","visibilityState","opened","step","enteredAt","done","idleTimer","idleReported","deadClicks","burst","edits","Map","dirty","Set","struggled","index","spent","Math","max","round","stopIdle","clearTimeout","armIdle","setTimeout","signal","duration_ms","edited","field","count","get","set","has","add","view","next","step_index","step_count","length","complete","back","fieldError","code","error","fieldChanged","fieldLeft","delete","fieldPicked","activity","click","at","near","hypot","x","y","reported","area","clicks","interactive","blank","abandon","reason","total_ms","finish","dispose","INTERACTIVE","join","AI_SITE_START_AREA_ATTRIBUTE","BACKDROP","isContent","element","closest","node","Array","from","childNodes","nodeType","textContent","trim","aiSiteStartClickOf","event","target","Element","named","getAttribute","Boolean","clientX","clientY","useAiSiteStartTracking","active","ref","current","tracker","leaving","onPageHide","window","addEventListener","removeEventListener"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  /**
4
4
  * @license
5
5
  * Copyright 2026 Aglyn LLC
@@ -37,6 +37,7 @@ import { AiInsightDialog } from "./ai-insight-dialog.component.js";
37
37
  import { AiJobPlan } from "./ai-job-plan.component.js";
38
38
  import { aiCreditsBillingHref, aiJobOutputHref, aiJobPrimaryLink, aiSiteBuildHref } from "./ai-job-links.js";
39
39
  import { aiJobRefundCopy } from "../model/ai-job-failure-copy.js";
40
+ import { AI_SITE_REMOVED_ROW_COPY, AI_SITE_START_AGAIN_LABEL, aiCreateSiteHref, stashAiSiteStartAnswers } from "../model/ai-site-start-handoff.js";
40
41
  import { readEventFrames } from "./ai-job-events.js";
41
42
  /** Try again, with what it is likely to cost (AGL-3722). */ function aiBuildRetryLabel(job) {
42
43
  const range = aiBuildRetryCreditRange(job);
@@ -612,7 +613,28 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
612
613
  })
613
614
  ]
614
615
  }) : null,
615
- restartHref(job) ? /*#__PURE__*/ _jsx(Button, {
616
+ job.siteRemoved ? // Its empty site was removed (AGL-3936): Create site, with the same answers.
617
+ /*#__PURE__*/ _jsxs(_Fragment, {
618
+ children: [
619
+ /*#__PURE__*/ _jsx(Typography, {
620
+ variant: "caption",
621
+ color: "text.secondary",
622
+ component: "div",
623
+ children: AI_SITE_REMOVED_ROW_COPY
624
+ }),
625
+ aiCreateSiteHref(orgSlug) && job.siteInputs ? /*#__PURE__*/ _jsx(Button, {
626
+ size: "small",
627
+ variant: "contained",
628
+ component: AppLink,
629
+ href: aiCreateSiteHref(orgSlug),
630
+ onClick: ()=>stashAiSiteStartAnswers(job.siteInputs),
631
+ sx: {
632
+ mt: 0.5
633
+ },
634
+ children: AI_SITE_START_AGAIN_LABEL
635
+ }) : null
636
+ ]
637
+ }) : restartHref(job) ? /*#__PURE__*/ _jsx(Button, {
616
638
  size: "small",
617
639
  variant: "contained",
618
640
  component: AppLink,