@aglyn/plugins-ai 1.0.0-beta.292 → 1.0.0-beta.296
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +15 -2
- package/src/lib/components/ai-job-links.js +24 -3
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +9 -0
- package/src/lib/components/ai-job-requests.js +34 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +28 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js +20 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
- package/src/lib/components/assist-page-change-card.component.js +166 -0
- package/src/lib/components/assist-page-change-card.component.js.map +1 -0
- package/src/lib/components/assist-panel.component.d.ts +105 -1
- package/src/lib/components/assist-panel.component.js +224 -29
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
- package/src/lib/jobs/ai-job-edit-step.js +172 -61
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.js +4 -2
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +33 -16
- package/src/lib/model/ai-edit-job.js +50 -34
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +6 -11
- package/src/lib/model/ai-job-activity.js +8 -15
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +6 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +6 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +18 -1
- package/src/lib/model/assist-edit.js +7 -0
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +86 -0
- package/src/lib/model/assist-page-change.js +86 -0
- package/src/lib/model/assist-page-change.js.map +1 -0
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval.js +3 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +5 -0
- package/src/lib/runtime/ai-node-tree.js +23 -4
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +8 -1
- package/src/lib/runtime/site-inventory.js +18 -0
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/assist-build.js +2 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.js +51 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +54 -2
- package/src/lib/server/assist-edit.js +219 -30
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.d.ts +62 -0
- package/src/lib/server/assist-page-change.js +140 -0
- package/src/lib/server/assist-page-change.js.map +1 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-steps.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\nimport { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from './ai-site-job'\nimport {\n aiSiteStartExtraProblem,\n aiSiteStartInspirationRefusal,\n type AiSiteStartAnswers,\n type AiSiteStartExtraKey,\n} from './ai-site-start'\n\n/**\n * The guided start's steps (AGL-3898): the same questions `ai-site-start.ts`\n * asks, one decision to a step, so a person answers one thing at a time and\n * the funnel can say which one they stopped at.\n *\n * Splitting the questions changes nothing they become. Every step writes the\n * same {@link AiSiteStartAnswers} the single form did, and only the review's\n * \"Plan my site\" turns them into the job's brief and inputs, through the same\n * `aiSiteStartBrief` / `aiSiteStartInputs`. Which answers are required is\n * unchanged too: only what kind of site it is.\n *\n * Pure: ids, titles, which answers each step holds, and each step's reasons\n * not to go forward, worded as `aiSiteStartRefusal` words them.\n */\n\n/** A step of the guided start, in order. `choose` is the starter-or-AI choice the questions follow. */\nexport const AI_SITE_START_STEPS = ['choose', 'kind', 'audience', 'style', 'details', 'review'] as const\n\nexport type AiSiteStartStep = (typeof AI_SITE_START_STEPS)[number]\n\n/** Each step's short name, as the progress indicator shows it. */\nexport const AI_SITE_START_STEP_LABELS: Readonly<Record<AiSiteStartStep, string>> = {\n choose: 'Start',\n kind: 'Your site',\n audience: 'Audience',\n style: 'Style',\n details: 'Details',\n review: 'Review',\n}\n\n/** An answer a step asks for, by its key on {@link AiSiteStartAnswers}. */\nexport type AiSiteStartField =\n | 'siteType'\n | 'audience'\n | 'kind'\n | 'inspiration'\n | 'pages'\n | 'submissions'\n | 'welcomeEmail'\n | AiSiteStartExtraKey\n\n/**\n * The extras each step's \"More details\" offers (AGL-3979), in the order it\n * draws them. The phone number and the email are on the Details step itself,\n * above where messages go: they are the two a new site most needs.\n */\nexport const AI_SITE_START_MORE_FIELDS: Readonly<Record<AiSiteStartStep, readonly AiSiteStartExtraKey[]>> = {\n choose: [],\n kind: ['tagline', 'founded'],\n audience: ['serviceArea', 'priceRange', 'languages'],\n style: ['brand'],\n details: ['street', 'city', 'region', 'postalCode', 'country', 'hours', 'social'],\n review: [],\n}\n\n/** The answers each step holds, its \"More details\" last. */\nexport const AI_SITE_START_STEP_FIELDS: Readonly<Record<AiSiteStartStep, readonly AiSiteStartField[]>> = {\n choose: [],\n kind: ['siteType', ...AI_SITE_START_MORE_FIELDS.kind],\n audience: ['audience', ...AI_SITE_START_MORE_FIELDS.audience],\n // The sites they like sit under the style cards (AGL-3975), when asked.\n style: ['kind', 'inspiration', ...AI_SITE_START_MORE_FIELDS.style],\n details: ['phone', 'email', 'submissions', 'pages', 'welcomeEmail', ...AI_SITE_START_MORE_FIELDS.details],\n review: [],\n}\n\n/** Why an answer cannot go forward, as a closed code the funnel can count. */\nexport type AiSiteStartErrorCode = 'required' | 'too_long' | 'out_of_range' | 'invalid'\n\n/** One answer's reason not to go forward: the sentence beside the field, and its code. */\nexport interface AiSiteStartFieldError {\n message: string\n code: AiSiteStartErrorCode\n}\n\n/** The step's place, from 0. */\nexport function aiSiteStartStepIndex(step: AiSiteStartStep): number {\n return AI_SITE_START_STEPS.indexOf(step)\n}\n\n/** The step after this one, or `null` after the review. */\nexport function aiSiteStartNextStep(step: AiSiteStartStep): AiSiteStartStep | null {\n return AI_SITE_START_STEPS[aiSiteStartStepIndex(step) + 1] ?? null\n}\n\n/** The step before this one, or `null` before the choice. */\nexport function aiSiteStartPreviousStep(step: AiSiteStartStep): AiSiteStartStep | null {\n const index = aiSiteStartStepIndex(step)\n return index > 0 ? (AI_SITE_START_STEPS[index - 1] ?? null) : null\n}\n\n/** Every answer's reason not to go forward, by field, with nothing for an answer that may. */\nexport function aiSiteStartFieldErrors(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): Partial<Record<AiSiteStartField, AiSiteStartFieldError>> {\n const errors: Partial<Record<AiSiteStartField, AiSiteStartFieldError>> = {}\n const siteType = answers.siteType.trim()\n if (!siteType) errors.siteType = { message: 'Say what kind of site this is.', code: 'required' }\n else if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n errors.siteType = { message: `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`, code: 'too_long' }\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n errors.audience = { message: `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`, code: 'too_long' }\n }\n const band = aiSitePagesBand(options.freeTaste)\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n errors.pages = { message: `A site is planned with ${band.min} to ${band.max} pages.`, code: 'out_of_range' }\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n errors.submissions = { message: 'Say where the contact form’s submissions go.', code: 'invalid' }\n }\n const inspiration = aiSiteStartInspirationRefusal(answers, options)\n if (inspiration) errors.inspiration = { message: inspiration, code: 'invalid' }\n // Every extra is optional; one given is held to its own rule (AGL-3979).\n for (const field of Object.values(AI_SITE_START_STEP_FIELDS).flat()) {\n if (field in errors) continue\n const problem = isExtra(field) ? aiSiteStartExtraProblem(field, answers[field] ?? '') : null\n if (problem) errors[field] = problem\n }\n return errors\n}\n\nconst CORE_FIELDS: ReadonlySet<AiSiteStartField> = new Set([\n 'siteType',\n 'audience',\n 'kind',\n 'inspiration',\n 'pages',\n 'submissions',\n 'welcomeEmail',\n])\n\nfunction isExtra(field: AiSiteStartField): field is AiSiteStartExtraKey {\n return !CORE_FIELDS.has(field)\n}\n\n/** One step's reasons not to go forward, in the order its fields are drawn; empty when it may. */\nexport function aiSiteStartStepErrors(\n step: AiSiteStartStep,\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): Array<{ field: AiSiteStartField } & AiSiteStartFieldError> {\n const errors = aiSiteStartFieldErrors(answers, options)\n return AI_SITE_START_STEP_FIELDS[step].flatMap((field) => {\n const error = errors[field]\n return error ? [{ field, ...error }] : []\n })\n}\n\n/** The first step holding an answer that cannot go forward, for a review that was reached with one; `null` for none. */\nexport function aiSiteStartFirstInvalidStep(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartStep | null {\n return AI_SITE_START_STEPS.find((step) => aiSiteStartStepErrors(step, answers, options).length > 0) ?? null\n}\n"],"names":["AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","aiSiteStartExtraProblem","aiSiteStartInspirationRefusal","AI_SITE_START_STEPS","AI_SITE_START_STEP_LABELS","choose","kind","audience","style","details","review","AI_SITE_START_MORE_FIELDS","AI_SITE_START_STEP_FIELDS","aiSiteStartStepIndex","step","indexOf","aiSiteStartNextStep","aiSiteStartPreviousStep","index","aiSiteStartFieldErrors","answers","options","errors","siteType","trim","message","code","length","band","freeTaste","Number","isInteger","pages","min","max","includes","submissions","inspiration","field","Object","values","flat","problem","isExtra","CORE_FIELDS","Set","has","aiSiteStartStepErrors","flatMap","error","aiSiteStartFirstInvalidStep","find"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-steps.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\nimport { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from './ai-site-job'\nimport {\n AI_SITE_START_STYLE_REQUIRED,\n aiSiteStartExtraProblem,\n aiSiteStartInspirationRefusal,\n type AiSiteStartAnswers,\n type AiSiteStartExtraKey,\n} from './ai-site-start'\n\n/**\n * The guided start's steps (AGL-3898): the same questions `ai-site-start.ts`\n * asks, one decision to a step, so a person answers one thing at a time and\n * the funnel can say which one they stopped at.\n *\n * Splitting the questions changes nothing they become. Every step writes the\n * same {@link AiSiteStartAnswers} the single form did, and only the review's\n * \"Plan my site\" turns them into the job's brief and inputs, through the same\n * `aiSiteStartBrief` / `aiSiteStartInputs`. Two answers are required: what\n * kind of site it is, and a style card clicked (AGL-4049; \"Pick for me\" is\n * one), since nothing is chosen for a person.\n *\n * Pure: ids, titles, which answers each step holds, and each step's reasons\n * not to go forward, worded as `aiSiteStartRefusal` words them.\n */\n\n/** A step of the guided start, in order. `choose` is the starter-or-AI choice the questions follow. */\nexport const AI_SITE_START_STEPS = ['choose', 'kind', 'audience', 'style', 'details', 'review'] as const\n\nexport type AiSiteStartStep = (typeof AI_SITE_START_STEPS)[number]\n\n/** Each step's short name, as the progress indicator shows it. */\nexport const AI_SITE_START_STEP_LABELS: Readonly<Record<AiSiteStartStep, string>> = {\n choose: 'Start',\n kind: 'Your site',\n audience: 'Audience',\n style: 'Style',\n details: 'Details',\n review: 'Review',\n}\n\n/** An answer a step asks for, by its key on {@link AiSiteStartAnswers}. */\nexport type AiSiteStartField =\n | 'siteType'\n | 'audience'\n | 'style'\n | 'kind'\n | 'inspiration'\n | 'pages'\n | 'submissions'\n | 'welcomeEmail'\n | AiSiteStartExtraKey\n\n/**\n * The extras each step's \"More details\" offers (AGL-3979), in the order it\n * draws them. The phone number and the email are on the Details step itself,\n * above where messages go: they are the two a new site most needs.\n */\nexport const AI_SITE_START_MORE_FIELDS: Readonly<Record<AiSiteStartStep, readonly AiSiteStartExtraKey[]>> = {\n choose: [],\n kind: ['tagline', 'founded'],\n audience: ['serviceArea', 'priceRange', 'languages'],\n style: ['brand'],\n details: ['street', 'city', 'region', 'postalCode', 'country', 'hours', 'social'],\n review: [],\n}\n\n/** The answers each step holds, its \"More details\" last. */\nexport const AI_SITE_START_STEP_FIELDS: Readonly<Record<AiSiteStartStep, readonly AiSiteStartField[]>> = {\n choose: [],\n kind: ['siteType', ...AI_SITE_START_MORE_FIELDS.kind],\n audience: ['audience', ...AI_SITE_START_MORE_FIELDS.audience],\n // The sites they like sit under the style cards (AGL-3975), when asked.\n style: ['style', 'kind', 'inspiration', ...AI_SITE_START_MORE_FIELDS.style],\n details: ['phone', 'email', 'submissions', 'pages', 'welcomeEmail', ...AI_SITE_START_MORE_FIELDS.details],\n review: [],\n}\n\n/** Why an answer cannot go forward, as a closed code the funnel can count. */\nexport type AiSiteStartErrorCode = 'required' | 'too_long' | 'out_of_range' | 'invalid'\n\n/** One answer's reason not to go forward: the sentence beside the field, and its code. */\nexport interface AiSiteStartFieldError {\n message: string\n code: AiSiteStartErrorCode\n}\n\n/** The step's place, from 0. */\nexport function aiSiteStartStepIndex(step: AiSiteStartStep): number {\n return AI_SITE_START_STEPS.indexOf(step)\n}\n\n/** The step after this one, or `null` after the review. */\nexport function aiSiteStartNextStep(step: AiSiteStartStep): AiSiteStartStep | null {\n return AI_SITE_START_STEPS[aiSiteStartStepIndex(step) + 1] ?? null\n}\n\n/** The step before this one, or `null` before the choice. */\nexport function aiSiteStartPreviousStep(step: AiSiteStartStep): AiSiteStartStep | null {\n const index = aiSiteStartStepIndex(step)\n return index > 0 ? (AI_SITE_START_STEPS[index - 1] ?? null) : null\n}\n\n/** Every answer's reason not to go forward, by field, with nothing for an answer that may. */\nexport function aiSiteStartFieldErrors(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): Partial<Record<AiSiteStartField, AiSiteStartFieldError>> {\n const errors: Partial<Record<AiSiteStartField, AiSiteStartFieldError>> = {}\n const siteType = answers.siteType.trim()\n if (!siteType) errors.siteType = { message: 'Say what kind of site this is.', code: 'required' }\n else if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n errors.siteType = { message: `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`, code: 'too_long' }\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n errors.audience = { message: `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`, code: 'too_long' }\n }\n // Nothing is chosen for a person (AGL-4049): a card, \"Pick for me\" included.\n if (answers.style === null) errors.style = { message: AI_SITE_START_STYLE_REQUIRED, code: 'required' }\n const band = aiSitePagesBand(options.freeTaste)\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n errors.pages = { message: `A site is planned with ${band.min} to ${band.max} pages.`, code: 'out_of_range' }\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n errors.submissions = { message: 'Say where the contact form’s submissions go.', code: 'invalid' }\n }\n const inspiration = aiSiteStartInspirationRefusal(answers, options)\n if (inspiration) errors.inspiration = { message: inspiration, code: 'invalid' }\n // Every extra is optional; one given is held to its own rule (AGL-3979).\n for (const field of Object.values(AI_SITE_START_STEP_FIELDS).flat()) {\n if (field in errors) continue\n const problem = isExtra(field) ? aiSiteStartExtraProblem(field, answers[field] ?? '') : null\n if (problem) errors[field] = problem\n }\n return errors\n}\n\nconst CORE_FIELDS: ReadonlySet<AiSiteStartField> = new Set([\n 'siteType',\n 'audience',\n 'style',\n 'kind',\n 'inspiration',\n 'pages',\n 'submissions',\n 'welcomeEmail',\n])\n\nfunction isExtra(field: AiSiteStartField): field is AiSiteStartExtraKey {\n return !CORE_FIELDS.has(field)\n}\n\n/** One step's reasons not to go forward, in the order its fields are drawn; empty when it may. */\nexport function aiSiteStartStepErrors(\n step: AiSiteStartStep,\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): Array<{ field: AiSiteStartField } & AiSiteStartFieldError> {\n const errors = aiSiteStartFieldErrors(answers, options)\n return AI_SITE_START_STEP_FIELDS[step].flatMap((field) => {\n const error = errors[field]\n return error ? [{ field, ...error }] : []\n })\n}\n\n/** The first step holding an answer that cannot go forward, for a review that was reached with one; `null` for none. */\nexport function aiSiteStartFirstInvalidStep(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartStep | null {\n return AI_SITE_START_STEPS.find((step) => aiSiteStartStepErrors(step, answers, options).length > 0) ?? null\n}\n"],"names":["AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_STYLE_REQUIRED","aiSiteStartExtraProblem","aiSiteStartInspirationRefusal","AI_SITE_START_STEPS","AI_SITE_START_STEP_LABELS","choose","kind","audience","style","details","review","AI_SITE_START_MORE_FIELDS","AI_SITE_START_STEP_FIELDS","aiSiteStartStepIndex","step","indexOf","aiSiteStartNextStep","aiSiteStartPreviousStep","index","aiSiteStartFieldErrors","answers","options","errors","siteType","trim","message","code","length","band","freeTaste","Number","isInteger","pages","min","max","includes","submissions","inspiration","field","Object","values","flat","problem","isExtra","CORE_FIELDS","Set","has","aiSiteStartStepErrors","flatMap","error","aiSiteStartFirstInvalidStep","find"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,EAAEC,mBAAmB,EAAEC,eAAe,QAAQ,mBAAe;AAC7F,SACEC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,QAGxB,qBAAiB;AAExB;;;;;;;;;;;;;;CAcC,GAED,qGAAqG,GACrG,OAAO,MAAMC,sBAAsB;IAAC;IAAU;IAAQ;IAAY;IAAS;IAAW;CAAS,CAAS;AAIxG,gEAAgE,GAChE,OAAO,MAAMC,4BAAuE;IAClFC,QAAQ;IACRC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,SAAS;IACTC,QAAQ;AACV,EAAC;AAcD;;;;CAIC,GACD,OAAO,MAAMC,4BAA+F;IAC1GN,QAAQ,EAAE;IACVC,MAAM;QAAC;QAAW;KAAU;IAC5BC,UAAU;QAAC;QAAe;QAAc;KAAY;IACpDC,OAAO;QAAC;KAAQ;IAChBC,SAAS;QAAC;QAAU;QAAQ;QAAU;QAAc;QAAW;QAAS;KAAS;IACjFC,QAAQ,EAAE;AACZ,EAAC;AAED,0DAA0D,GAC1D,OAAO,MAAME,4BAA4F;IACvGP,QAAQ,EAAE;IACVC,MAAM;QAAC;WAAeK,0BAA0BL,IAAI;KAAC;IACrDC,UAAU;QAAC;WAAeI,0BAA0BJ,QAAQ;KAAC;IAC7D,wEAAwE;IACxEC,OAAO;QAAC;QAAS;QAAQ;WAAkBG,0BAA0BH,KAAK;KAAC;IAC3EC,SAAS;QAAC;QAAS;QAAS;QAAe;QAAS;WAAmBE,0BAA0BF,OAAO;KAAC;IACzGC,QAAQ,EAAE;AACZ,EAAC;AAWD,8BAA8B,GAC9B,OAAO,SAASG,qBAAqBC,IAAqB;IACxD,OAAOX,oBAAoBY,OAAO,CAACD;AACrC;AAEA,yDAAyD,GACzD,OAAO,SAASE,oBAAoBF,IAAqB;QAChDX;IAAP,QAAOA,wBAAAA,mBAAmB,CAACU,qBAAqBC,QAAQ,EAAE,YAAnDX,wBAAuD;AAChE;AAEA,2DAA2D,GAC3D,OAAO,SAASc,wBAAwBH,IAAqB;QAEvCX;IADpB,MAAMe,QAAQL,qBAAqBC;IACnC,OAAOI,QAAQ,KAAKf,wBAAAA,mBAAmB,CAACe,QAAQ,EAAE,YAA9Bf,wBAAkC,OAAQ;AAChE;AAEA,4FAA4F,GAC5F,OAAO,SAASgB,uBACdC,OAA2B,EAC3BC,UAAmC,CAAC,CAAC;IAErC,MAAMC,SAAmE,CAAC;IAC1E,MAAMC,WAAWH,QAAQG,QAAQ,CAACC,IAAI;IACtC,IAAI,CAACD,UAAUD,OAAOC,QAAQ,GAAG;QAAEE,SAAS;QAAkCC,MAAM;IAAW;SAC1F,IAAIH,SAASI,MAAM,GAAG9B,yBAAyB;QAClDyB,OAAOC,QAAQ,GAAG;YAAEE,SAAS,CAAC,qCAAqC,EAAE5B,wBAAwB,YAAY,CAAC;YAAE6B,MAAM;QAAW;IAC/H;IACA,IAAIN,QAAQb,QAAQ,CAACiB,IAAI,GAAGG,MAAM,GAAG9B,yBAAyB;QAC5DyB,OAAOf,QAAQ,GAAG;YAAEkB,SAAS,CAAC,+BAA+B,EAAE5B,wBAAwB,YAAY,CAAC;YAAE6B,MAAM;QAAW;IACzH;IACA,6EAA6E;IAC7E,IAAIN,QAAQZ,KAAK,KAAK,MAAMc,OAAOd,KAAK,GAAG;QAAEiB,SAASzB;QAA8B0B,MAAM;IAAW;IACrG,MAAME,OAAO7B,gBAAgBsB,QAAQQ,SAAS;IAC9C,IAAI,CAACC,OAAOC,SAAS,CAACX,QAAQY,KAAK,KAAKZ,QAAQY,KAAK,GAAGJ,KAAKK,GAAG,IAAIb,QAAQY,KAAK,GAAGJ,KAAKM,GAAG,EAAE;QAC5FZ,OAAOU,KAAK,GAAG;YAAEP,SAAS,CAAC,uBAAuB,EAAEG,KAAKK,GAAG,CAAC,IAAI,EAAEL,KAAKM,GAAG,CAAC,OAAO,CAAC;YAAER,MAAM;QAAe;IAC7G;IACA,IAAI,CAAC,AAAC5B,oBAA0CqC,QAAQ,CAACf,QAAQgB,WAAW,GAAG;QAC7Ed,OAAOc,WAAW,GAAG;YAAEX,SAAS;YAAgDC,MAAM;QAAU;IAClG;IACA,MAAMW,cAAcnC,8BAA8BkB,SAASC;IAC3D,IAAIgB,aAAaf,OAAOe,WAAW,GAAG;QAAEZ,SAASY;QAAaX,MAAM;IAAU;IAC9E,yEAAyE;IACzE,KAAK,MAAMY,SAASC,OAAOC,MAAM,CAAC5B,2BAA2B6B,IAAI,GAAI;YAEHrB;QADhE,IAAIkB,SAAShB,QAAQ;QACrB,MAAMoB,UAAUC,QAAQL,SAASrC,wBAAwBqC,QAAOlB,iBAAAA,OAAO,CAACkB,MAAM,YAAdlB,iBAAkB,MAAM;QACxF,IAAIsB,SAASpB,MAAM,CAACgB,MAAM,GAAGI;IAC/B;IACA,OAAOpB;AACT;AAEA,MAAMsB,cAA6C,IAAIC,IAAI;IACzD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,SAASF,QAAQL,KAAuB;IACtC,OAAO,CAACM,YAAYE,GAAG,CAACR;AAC1B;AAEA,gGAAgG,GAChG,OAAO,SAASS,sBACdjC,IAAqB,EACrBM,OAA2B,EAC3BC,UAAmC,CAAC,CAAC;IAErC,MAAMC,SAASH,uBAAuBC,SAASC;IAC/C,OAAOT,yBAAyB,CAACE,KAAK,CAACkC,OAAO,CAAC,CAACV;QAC9C,MAAMW,QAAQ3B,MAAM,CAACgB,MAAM;QAC3B,OAAOW,QAAQ;YAAC;gBAAEX;eAAUW;SAAQ,GAAG,EAAE;IAC3C;AACF;AAEA,sHAAsH,GACtH,OAAO,SAASC,4BACd9B,OAA2B,EAC3BC,UAAmC,CAAC,CAAC;QAE9BlB;IAAP,QAAOA,4BAAAA,oBAAoBgD,IAAI,CAAC,CAACrC,OAASiC,sBAAsBjC,MAAMM,SAASC,SAASM,MAAM,GAAG,cAA1FxB,4BAAgG;AACzG"}
|
|
@@ -89,10 +89,17 @@ export interface AiSiteStartAnswers extends AiSiteDetails {
|
|
|
89
89
|
audience: string;
|
|
90
90
|
/** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */
|
|
91
91
|
example: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* The style card they clicked (AGL-4049): a kind's id, or
|
|
94
|
+
* `SITE_STYLE_AUTO` for "Pick for me"; `null` while they have clicked none.
|
|
95
|
+
* Nothing is chosen for a person, so the Style step does not go forward on
|
|
96
|
+
* `null` ({@link aiSiteStartStyleAnswer} sets this and {@link kind} together).
|
|
97
|
+
*/
|
|
98
|
+
style: string | null;
|
|
92
99
|
/**
|
|
93
100
|
* The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;
|
|
94
|
-
* `null`
|
|
95
|
-
* the one used ({@link aiSiteStartKind}).
|
|
101
|
+
* `null` for "Pick for me", or before a style is chosen, when the kind
|
|
102
|
+
* their first answer suggests is the one used ({@link aiSiteStartKind}).
|
|
96
103
|
*/
|
|
97
104
|
kind: string | null;
|
|
98
105
|
/** How many pages to plan. */
|
|
@@ -119,6 +126,14 @@ export interface AiSiteStartAnswers extends AiSiteDetails {
|
|
|
119
126
|
* thing a person should have to choose.
|
|
120
127
|
*/
|
|
121
128
|
export declare const AI_SITE_START_ANSWERS: AiSiteStartAnswers;
|
|
129
|
+
/**
|
|
130
|
+
* Why the Style step does not go forward before a card is clicked (AGL-4049),
|
|
131
|
+
* and why the review does not plan: nothing is chosen for a person, and
|
|
132
|
+
* "Pick for me" is a card too. The door itself admits a start without one
|
|
133
|
+
* ({@link aiSiteStartRefusal} is what it would refuse), so this is the
|
|
134
|
+
* questions' rule, held by `aiSiteStartStepErrors` and the card.
|
|
135
|
+
*/
|
|
136
|
+
export declare const AI_SITE_START_STYLE_REQUIRED = "Choose a style, or Pick for me.";
|
|
122
137
|
/** The answers a person may give beyond the ones each step asks, by key (AGL-3979). */
|
|
123
138
|
export type AiSiteStartExtraKey = AiSiteDetailKey | 'city' | 'brand';
|
|
124
139
|
/** Every extra, in the order the review lists them. */
|
|
@@ -130,6 +145,12 @@ export declare function aiSiteStartExtraProblem(key: AiSiteStartExtraKey, value:
|
|
|
130
145
|
} | null;
|
|
131
146
|
/** The extras a person gave, in the review's order: what "More details" was used for. */
|
|
132
147
|
export declare function aiSiteStartExtrasGiven(answers: AiSiteStartAnswers): AiSiteStartExtraKey[];
|
|
148
|
+
/**
|
|
149
|
+
* A style card clicked (AGL-4049), as the answers hold it: the card's id as
|
|
150
|
+
* the style, and the kind it names — none for "Pick for me", which leaves the
|
|
151
|
+
* kind to what the site is and who it is for.
|
|
152
|
+
*/
|
|
153
|
+
export declare function aiSiteStartStyleAnswer(id: string): Pick<AiSiteStartAnswers, 'style' | 'kind'>;
|
|
133
154
|
/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */
|
|
134
155
|
export declare function aiSiteStartKind(answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>): AiSiteKind;
|
|
135
156
|
/** The example an answer names, or `null` when it names none this list knows. */
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { SITE_STYLE_AUTO } from "@aglyn/aglyn/app-utils/site-styles";
|
|
17
18
|
import { weeklyHoursFromText, weeklyHoursToText } from "@aglyn/shared-ui-jsx/utils/weekly-hours";
|
|
18
19
|
import { AI_SITE_DETAIL_KEYS, AI_SITE_NO_DETAILS, aiSiteDetailProblem, aiSiteDetailsOf } from "./ai-site-details.js";
|
|
19
20
|
import { aiSiteKind, aiSiteKindFor } from "./ai-site-kinds.js";
|
|
@@ -79,6 +80,7 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
79
80
|
siteType: '',
|
|
80
81
|
audience: '',
|
|
81
82
|
example: null,
|
|
83
|
+
style: null,
|
|
82
84
|
kind: null,
|
|
83
85
|
pages: 5,
|
|
84
86
|
submissions: 'inbox',
|
|
@@ -86,6 +88,13 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
86
88
|
city: '',
|
|
87
89
|
brand: ''
|
|
88
90
|
}, AI_SITE_NO_DETAILS);
|
|
91
|
+
/**
|
|
92
|
+
* Why the Style step does not go forward before a card is clicked (AGL-4049),
|
|
93
|
+
* and why the review does not plan: nothing is chosen for a person, and
|
|
94
|
+
* "Pick for me" is a card too. The door itself admits a start without one
|
|
95
|
+
* ({@link aiSiteStartRefusal} is what it would refuse), so this is the
|
|
96
|
+
* questions' rule, held by `aiSiteStartStepErrors` and the card.
|
|
97
|
+
*/ export const AI_SITE_START_STYLE_REQUIRED = 'Choose a style, or Pick for me.';
|
|
89
98
|
/** Every extra, in the order the review lists them. */ export const AI_SITE_START_EXTRA_KEYS = [
|
|
90
99
|
'phone',
|
|
91
100
|
'email',
|
|
@@ -118,6 +127,18 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
118
127
|
return ((_answers_key = answers[key]) != null ? _answers_key : '').trim() !== '';
|
|
119
128
|
});
|
|
120
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* A style card clicked (AGL-4049), as the answers hold it: the card's id as
|
|
132
|
+
* the style, and the kind it names — none for "Pick for me", which leaves the
|
|
133
|
+
* kind to what the site is and who it is for.
|
|
134
|
+
*/ export function aiSiteStartStyleAnswer(id) {
|
|
135
|
+
var _ref;
|
|
136
|
+
var _aiSiteKind;
|
|
137
|
+
return {
|
|
138
|
+
style: id,
|
|
139
|
+
kind: id === SITE_STYLE_AUTO ? null : (_ref = (_aiSiteKind = aiSiteKind(id)) == null ? void 0 : _aiSiteKind.id) != null ? _ref : null
|
|
140
|
+
};
|
|
141
|
+
}
|
|
121
142
|
/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */ export function aiSiteStartKind(answers) {
|
|
122
143
|
var _aiSiteKind, _answers_audience;
|
|
123
144
|
return (_aiSiteKind = aiSiteKind(answers.kind)) != null ? _aiSiteKind : aiSiteKindFor(answers.siteType, (_answers_audience = answers.audience) != null ? _answers_audience : '');
|
|
@@ -283,8 +304,8 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
283
304
|
return _extends({
|
|
284
305
|
siteType,
|
|
285
306
|
audience: text('audience'),
|
|
286
|
-
example: AI_SITE_START_EXAMPLES.some((entry)=>entry.id === starter) ? starter : null
|
|
287
|
-
|
|
307
|
+
example: AI_SITE_START_EXAMPLES.some((entry)=>entry.id === starter) ? starter : null
|
|
308
|
+
}, aiSiteStartStyleAnswer((_ref = (_aiSiteKind = aiSiteKind(text('siteKind'))) == null ? void 0 : _aiSiteKind.id) != null ? _ref : SITE_STYLE_AUTO), {
|
|
288
309
|
pages: Math.min(band.max, Math.max(band.min, pages)),
|
|
289
310
|
submissions: AI_SITE_SUBMISSIONS.includes(submissions) ? submissions : AI_SITE_START_ANSWERS.submissions,
|
|
290
311
|
welcomeEmail: options.freeTaste ? false : (inputs == null ? void 0 : inputs['welcomeEmail']) !== false,
|
|
@@ -346,7 +367,8 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
346
367
|
hours: clientBuildHours(intake.hours)
|
|
347
368
|
});
|
|
348
369
|
return _extends({}, AI_SITE_START_ANSWERS, {
|
|
349
|
-
siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS)
|
|
370
|
+
siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS)
|
|
371
|
+
}, aiSiteStartStyleAnswer(SITE_STYLE_AUTO), {
|
|
350
372
|
phone: details.phone,
|
|
351
373
|
email: details.email,
|
|
352
374
|
serviceArea: details.serviceArea,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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\nimport type { ClientBuildIntake } from '@aglyn/aglyn/app-utils/client-build'\nimport { weeklyHoursFromText, weeklyHoursToText } from '@aglyn/shared-ui-jsx/utils/weekly-hours'\nimport {\n AI_SITE_DETAIL_KEYS,\n AI_SITE_NO_DETAILS,\n aiSiteDetailProblem,\n aiSiteDetailsOf,\n type AiSiteDetailKey,\n type AiSiteDetails,\n} from './ai-site-details'\nimport { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INSPIRATION_INPUT,\n aiSiteInspirationInput,\n aiSiteInspirationLimit,\n aiSiteInspirationRefusal,\n aiSiteInspirationUrl,\n} from './ai-site-inspiration'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/**\n * What a person answered; the shape the card holds and the brief is built from.\n *\n * Beside the answers each step asks, the business details its \"More details\"\n * offers (AGL-3979, `AiSiteDetails`): every one optional and `''` until given,\n * the phone number and email on the Details step itself.\n */\nexport interface AiSiteStartAnswers extends AiSiteDetails {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` until they pick one, when the kind their first answer suggests is\n * the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /** The city or town the business is in (AGL-3979); empty when they did not say. */\n city: string\n /** The brand color, as a six-digit hex (AGL-3979); empty for the style's own. */\n brand: string\n /**\n * The sites they like (AGL-3975), as typed, one a field; absent or empty\n * for none. Asked only while `release_ai_site_inspiration` is on.\n */\n inspiration?: string[]\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n city: '',\n brand: '',\n ...AI_SITE_NO_DETAILS,\n}\n\n/** The answers a person may give beyond the ones each step asks, by key (AGL-3979). */\nexport type AiSiteStartExtraKey = AiSiteDetailKey | 'city' | 'brand'\n\n/** Every extra, in the order the review lists them. */\nexport const AI_SITE_START_EXTRA_KEYS: readonly AiSiteStartExtraKey[] = [\n 'phone',\n 'email',\n 'tagline',\n 'founded',\n 'serviceArea',\n 'priceRange',\n 'languages',\n 'brand',\n 'street',\n 'city',\n 'region',\n 'postalCode',\n 'country',\n 'hours',\n 'social',\n]\n\n/** What is wrong with one extra, worded for beside its field; `null` when it may be used. */\nexport function aiSiteStartExtraProblem(\n key: AiSiteStartExtraKey,\n value: string,\n): { code: 'too_long' | 'invalid'; message: string } | null {\n if (key === 'city' || key === 'brand') {\n return value.trim().length > AI_SITE_INPUT_MAX_CHARS\n ? { code: 'too_long', message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.` }\n : null\n }\n return aiSiteDetailProblem(key, value)\n}\n\n/** The extras a person gave, in the review's order: what \"More details\" was used for. */\nexport function aiSiteStartExtrasGiven(answers: AiSiteStartAnswers): AiSiteStartExtraKey[] {\n return AI_SITE_START_EXTRA_KEYS.filter((key) => (answers[key] ?? '').trim() !== '')\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n for (const key of AI_SITE_START_EXTRA_KEYS) {\n const problem = aiSiteStartExtraProblem(key, answers[key] ?? '')\n if (problem) return problem.message\n }\n return aiSiteStartInspirationRefusal(answers, options)\n}\n\n/** The sites they typed, without the empty fields. */\nexport function aiSiteStartInspirationTyped(answers: Pick<AiSiteStartAnswers, 'inspiration'>): string[] {\n return (answers.inspiration ?? []).map((entry) => entry.trim()).filter(Boolean)\n}\n\n/** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */\nexport function aiSiteStartInspirationRefusal(\n answers: Pick<AiSiteStartAnswers, 'inspiration'>,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const typed = aiSiteStartInspirationTyped(answers)\n if (typed.some((entry) => !aiSiteInspirationUrl(entry))) {\n return 'Give each site you like as its https address, like https://example.com.'\n }\n return aiSiteInspirationRefusal(typed, options.freeTaste)\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId' | 'inspiration' | AiSiteDetailKey> & Partial<AiSiteDetails> & { inspiration?: string } {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n // The details are sent only where given (AGL-3979): the plan step lists\n // every scalar input on a line of its own, and a start without them is\n // asked exactly what it was asked before.\n const details: Partial<AiSiteDetails> = {}\n for (const key of AI_SITE_DETAIL_KEYS) {\n const value = (answers[key] ?? '').trim()\n if (value) details[key] = value\n }\n // The sites they like travel only when there are some (AGL-3975), so a\n // start without them asks for exactly what it always did.\n const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers))\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: (answers.city ?? '').trim(),\n brand: (answers.brand ?? '').trim(),\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n ...details,\n ...(inspiration ? { [AI_SITE_INSPIRATION_INPUT]: inspiration } : {}),\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n ...AI_SITE_DETAIL_KEYS,\n AI_SITE_INSPIRATION_INPUT,\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n kind: aiSiteKind(text('siteKind'))?.id ?? null,\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n city: text('city'),\n brand: text('brand'),\n ...(Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key) => [key, text(key)])) as unknown as AiSiteDetails),\n // The sites they liked come back as typed, within the workspace's limit (AGL-3975).\n ...(text(AI_SITE_INSPIRATION_INPUT)\n ? { inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste)) }\n : {}),\n }\n}\n\n// ── A build staff run for a client (AGL-3958) ───────────────────────────────\n\n/** Shortens an answer to what a scalar input admits, on a word where it can. */\nfunction bounded(value: string, max: number): string {\n const trimmed = value.trim().replace(/\\s+/g, ' ')\n if (trimmed.length <= max) return trimmed\n const cut = trimmed.slice(0, max)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : cut).trim()\n}\n\n/** A time with am or pm: \"9am\", \"5:30 pm\", \"12 a.m.\". */\nconst TWELVE_HOUR_TIME = /\\b(\\d{1,2})(?::(\\d{2}))?\\s*([ap])\\.?m\\.?(?![a-z])/gi\n\n/**\n * The hours an owner gave, in the shape the Details step's hours picker\n * writes (`Tu-Sa 09:00-17:00`, one set of days a line), so the plan reads\n * them as it reads hours picked there. A time with am or pm is the time it\n * names, \"to\" joins a range as a dash does, and days with their hours may be\n * set apart by a comma, a semicolon or a line. Whatever still does not read\n * as hours — \"8–5\", \"Sat by appointment\" — is left out rather than guessed at.\n */\nfunction clientBuildHours(hours: string): string {\n const lines = hours\n .replace(TWELVE_HOUR_TIME, (time, hour: string, minutes: string | undefined, half: string) => {\n const h = Number(hour)\n if (h < 1 || h > 12) return time\n const clock = (h % 12) + (half.toLowerCase() === 'p' ? 12 : 0)\n return `${String(clock).padStart(2, '0')}:${minutes ?? '00'}`\n })\n .replace(/\\s+to\\s+/gi, '-')\n .replace(/(\\d{1,2}:\\d{2})\\s*,\\s*/g, '$1\\n')\n .replace(/;/g, '\\n')\n const week = weeklyHoursFromText(lines).hours\n return week.some((day) => day.length) ? weeklyHoursToText(week) : ''\n}\n\n/**\n * The towns an intake serves, one a line, as the Details step's \"Areas you\n * serve\" holds them: \"Austin, Round Rock and Pflugerville\" is three.\n */\nfunction clientBuildServiceArea(towns: string): string {\n return towns\n .split(/\\r?\\n|[,;]|\\s+(?:and|&)\\s+/i)\n .map((town) => town.trim())\n .filter(Boolean)\n .join('\\n')\n}\n\n/**\n * The questions' answers for a site staff are building for a client: what\n * the business does is the kind of site, and the details the intake holds\n * fill the Details step's own fields (AGL-3979, AGL-3995): the phone, the\n * email, the towns as the areas it serves and the hours, each held to that\n * detail's own rule, so one the Details step would refuse is left empty\n * there. Every other answer keeps its default, which the review shows to\n * change. The intake, a refused detail included, also reaches the job\n * through {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.\n */\nexport function aiSiteStartAnswersFromClientBuild(intake: ClientBuildIntake): AiSiteStartAnswers {\n const details = aiSiteDetailsOf({\n phone: intake.phone,\n email: intake.email,\n serviceArea: clientBuildServiceArea(intake.towns),\n hours: clientBuildHours(intake.hours),\n })\n return {\n ...AI_SITE_START_ANSWERS,\n siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS),\n phone: details.phone,\n email: details.email,\n serviceArea: details.serviceArea,\n hours: details.hours,\n }\n}\n\n/**\n * The per-site inputs the intake fills, which the guided start's own\n * questions leave empty: the business's name for the header and footer, and\n * the towns it serves as its place.\n *\n * The towns are the place only while no address is given. The Entity reads\n * `city` as the address's own locality wherever there is a street or a\n * postal code (`aiSiteEntity`), so once the answers carry either, `city` is\n * the city the Details step was given with that address, never the towns\n * (AGL-3995). The intake asks no address, so today that is the Details step's\n * own, typed on the review; the towns still reach the site as the areas it\n * serves.\n */\nexport function aiSiteStartClientInputs(\n intake: ClientBuildIntake,\n answers?: Pick<AiSiteStartAnswers, 'street' | 'postalCode' | 'city'> | null,\n): Pick<AiSiteJobInputs, 'businessName' | 'city'> {\n const addressed = Boolean(answers?.street.trim() || answers?.postalCode.trim())\n return {\n businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),\n city: bounded(addressed ? (answers?.city ?? '') : intake.towns, AI_SITE_INPUT_MAX_CHARS),\n }\n}\n\n/**\n * The intake as sentences added to the brief: how to reach the business and\n * when it is open, which the site should say where a visitor looks for them,\n * and the owner's own words, quoted, which the copy should sound like. Empty\n * answers say nothing.\n */\nexport function aiSiteStartClientBrief(intake: ClientBuildIntake): string {\n const name = intake.businessName.trim()\n const towns = intake.towns.trim()\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n const hours = intake.hours.trim()\n const words = intake.ownerWords.trim().replace(/\\s+/g, ' ')\n const sentences: string[] = []\n if (name) sentences.push(`The business is called ${name}.`)\n if (towns) sentences.push(`It serves ${towns}.`)\n const contact = [phone ? `call ${phone}` : '', email ? `email ${email}` : ''].filter(Boolean)\n if (contact.length) {\n sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`)\n }\n if (hours) sentences.push(`Its hours are ${hours}.`)\n if (words) sentences.push(`In the owner's own words: \"${words}\" Write the copy so it sounds like them.`)\n return sentences.join(' ')\n}\n\n/**\n * What the review says the client build adds beyond its own rows, so staff\n * see the intake is used without it being asked again: the business's name\n * and the owner's own words, which no row shows. The phone, email, towns and\n * hours are the review's Details rows already (AGL-3995), so the note does\n * not say them twice: \"Also from the client's details: Harbor Plumbing and\n * the owner's own words.\"\n */\nexport function aiSiteStartClientNote(intake: ClientBuildIntake): string {\n const parts = [intake.businessName.trim(), intake.ownerWords.trim() ? \"the owner's own words\" : ''].filter(Boolean)\n if (!parts.length) return 'Built from the client’s details.'\n return `Also from the client’s details: ${parts.join(' and ')}.`\n}\n"],"names":["weeklyHoursFromText","weeklyHoursToText","AI_SITE_DETAIL_KEYS","AI_SITE_NO_DETAILS","aiSiteDetailProblem","aiSiteDetailsOf","aiSiteKind","aiSiteKindFor","AI_SITE_INSPIRATION_INPUT","aiSiteInspirationInput","aiSiteInspirationLimit","aiSiteInspirationRefusal","aiSiteInspirationUrl","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","kind","pages","submissions","welcomeEmail","city","brand","AI_SITE_START_EXTRA_KEYS","aiSiteStartExtraProblem","key","value","trim","length","code","message","aiSiteStartExtrasGiven","answers","filter","aiSiteStartKind","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","Number","isInteger","min","max","includes","problem","aiSiteStartInspirationRefusal","aiSiteStartInspirationTyped","inspiration","map","Boolean","typed","some","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","details","businessType","starter","siteKind","businessName","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","fromEntries","split","slice","bounded","trimmed","replace","cut","space","lastIndexOf","TWELVE_HOUR_TIME","clientBuildHours","hours","lines","time","hour","minutes","half","h","clock","String","padStart","week","day","clientBuildServiceArea","towns","town","aiSiteStartAnswersFromClientBuild","intake","phone","email","serviceArea","whatTheyDo","aiSiteStartClientInputs","addressed","street","postalCode","aiSiteStartClientBrief","name","words","ownerWords","contact","aiSiteStartClientNote","parts"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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\nimport type { ClientBuildIntake } from '@aglyn/aglyn/app-utils/client-build'\nimport { SITE_STYLE_AUTO } from '@aglyn/aglyn/app-utils/site-styles'\nimport { weeklyHoursFromText, weeklyHoursToText } from '@aglyn/shared-ui-jsx/utils/weekly-hours'\nimport {\n AI_SITE_DETAIL_KEYS,\n AI_SITE_NO_DETAILS,\n aiSiteDetailProblem,\n aiSiteDetailsOf,\n type AiSiteDetailKey,\n type AiSiteDetails,\n} from './ai-site-details'\nimport { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INSPIRATION_INPUT,\n aiSiteInspirationInput,\n aiSiteInspirationLimit,\n aiSiteInspirationRefusal,\n aiSiteInspirationUrl,\n} from './ai-site-inspiration'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/**\n * What a person answered; the shape the card holds and the brief is built from.\n *\n * Beside the answers each step asks, the business details its \"More details\"\n * offers (AGL-3979, `AiSiteDetails`): every one optional and `''` until given,\n * the phone number and email on the Details step itself.\n */\nexport interface AiSiteStartAnswers extends AiSiteDetails {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The style card they clicked (AGL-4049): a kind's id, or\n * `SITE_STYLE_AUTO` for \"Pick for me\"; `null` while they have clicked none.\n * Nothing is chosen for a person, so the Style step does not go forward on\n * `null` ({@link aiSiteStartStyleAnswer} sets this and {@link kind} together).\n */\n style: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` for \"Pick for me\", or before a style is chosen, when the kind\n * their first answer suggests is the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /** The city or town the business is in (AGL-3979); empty when they did not say. */\n city: string\n /** The brand color, as a six-digit hex (AGL-3979); empty for the style's own. */\n brand: string\n /**\n * The sites they like (AGL-3975), as typed, one a field; absent or empty\n * for none. Asked only while `release_ai_site_inspiration` is on.\n */\n inspiration?: string[]\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n style: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n city: '',\n brand: '',\n ...AI_SITE_NO_DETAILS,\n}\n\n/**\n * Why the Style step does not go forward before a card is clicked (AGL-4049),\n * and why the review does not plan: nothing is chosen for a person, and\n * \"Pick for me\" is a card too. The door itself admits a start without one\n * ({@link aiSiteStartRefusal} is what it would refuse), so this is the\n * questions' rule, held by `aiSiteStartStepErrors` and the card.\n */\nexport const AI_SITE_START_STYLE_REQUIRED = 'Choose a style, or Pick for me.'\n\n/** The answers a person may give beyond the ones each step asks, by key (AGL-3979). */\nexport type AiSiteStartExtraKey = AiSiteDetailKey | 'city' | 'brand'\n\n/** Every extra, in the order the review lists them. */\nexport const AI_SITE_START_EXTRA_KEYS: readonly AiSiteStartExtraKey[] = [\n 'phone',\n 'email',\n 'tagline',\n 'founded',\n 'serviceArea',\n 'priceRange',\n 'languages',\n 'brand',\n 'street',\n 'city',\n 'region',\n 'postalCode',\n 'country',\n 'hours',\n 'social',\n]\n\n/** What is wrong with one extra, worded for beside its field; `null` when it may be used. */\nexport function aiSiteStartExtraProblem(\n key: AiSiteStartExtraKey,\n value: string,\n): { code: 'too_long' | 'invalid'; message: string } | null {\n if (key === 'city' || key === 'brand') {\n return value.trim().length > AI_SITE_INPUT_MAX_CHARS\n ? { code: 'too_long', message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.` }\n : null\n }\n return aiSiteDetailProblem(key, value)\n}\n\n/** The extras a person gave, in the review's order: what \"More details\" was used for. */\nexport function aiSiteStartExtrasGiven(answers: AiSiteStartAnswers): AiSiteStartExtraKey[] {\n return AI_SITE_START_EXTRA_KEYS.filter((key) => (answers[key] ?? '').trim() !== '')\n}\n\n/**\n * A style card clicked (AGL-4049), as the answers hold it: the card's id as\n * the style, and the kind it names — none for \"Pick for me\", which leaves the\n * kind to what the site is and who it is for.\n */\nexport function aiSiteStartStyleAnswer(id: string): Pick<AiSiteStartAnswers, 'style' | 'kind'> {\n return { style: id, kind: id === SITE_STYLE_AUTO ? null : (aiSiteKind(id)?.id ?? null) }\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n for (const key of AI_SITE_START_EXTRA_KEYS) {\n const problem = aiSiteStartExtraProblem(key, answers[key] ?? '')\n if (problem) return problem.message\n }\n return aiSiteStartInspirationRefusal(answers, options)\n}\n\n/** The sites they typed, without the empty fields. */\nexport function aiSiteStartInspirationTyped(answers: Pick<AiSiteStartAnswers, 'inspiration'>): string[] {\n return (answers.inspiration ?? []).map((entry) => entry.trim()).filter(Boolean)\n}\n\n/** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */\nexport function aiSiteStartInspirationRefusal(\n answers: Pick<AiSiteStartAnswers, 'inspiration'>,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const typed = aiSiteStartInspirationTyped(answers)\n if (typed.some((entry) => !aiSiteInspirationUrl(entry))) {\n return 'Give each site you like as its https address, like https://example.com.'\n }\n return aiSiteInspirationRefusal(typed, options.freeTaste)\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId' | 'inspiration' | AiSiteDetailKey> & Partial<AiSiteDetails> & { inspiration?: string } {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n // The details are sent only where given (AGL-3979): the plan step lists\n // every scalar input on a line of its own, and a start without them is\n // asked exactly what it was asked before.\n const details: Partial<AiSiteDetails> = {}\n for (const key of AI_SITE_DETAIL_KEYS) {\n const value = (answers[key] ?? '').trim()\n if (value) details[key] = value\n }\n // The sites they like travel only when there are some (AGL-3975), so a\n // start without them asks for exactly what it always did.\n const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers))\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: (answers.city ?? '').trim(),\n brand: (answers.brand ?? '').trim(),\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n ...details,\n ...(inspiration ? { [AI_SITE_INSPIRATION_INPUT]: inspiration } : {}),\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n ...AI_SITE_DETAIL_KEYS,\n AI_SITE_INSPIRATION_INPUT,\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n // A start that ran had its style chosen: its kind, or \"Pick for me\".\n ...aiSiteStartStyleAnswer(aiSiteKind(text('siteKind'))?.id ?? SITE_STYLE_AUTO),\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n city: text('city'),\n brand: text('brand'),\n ...(Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key) => [key, text(key)])) as unknown as AiSiteDetails),\n // The sites they liked come back as typed, within the workspace's limit (AGL-3975).\n ...(text(AI_SITE_INSPIRATION_INPUT)\n ? { inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste)) }\n : {}),\n }\n}\n\n// ── A build staff run for a client (AGL-3958) ───────────────────────────────\n\n/** Shortens an answer to what a scalar input admits, on a word where it can. */\nfunction bounded(value: string, max: number): string {\n const trimmed = value.trim().replace(/\\s+/g, ' ')\n if (trimmed.length <= max) return trimmed\n const cut = trimmed.slice(0, max)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : cut).trim()\n}\n\n/** A time with am or pm: \"9am\", \"5:30 pm\", \"12 a.m.\". */\nconst TWELVE_HOUR_TIME = /\\b(\\d{1,2})(?::(\\d{2}))?\\s*([ap])\\.?m\\.?(?![a-z])/gi\n\n/**\n * The hours an owner gave, in the shape the Details step's hours picker\n * writes (`Tu-Sa 09:00-17:00`, one set of days a line), so the plan reads\n * them as it reads hours picked there. A time with am or pm is the time it\n * names, \"to\" joins a range as a dash does, and days with their hours may be\n * set apart by a comma, a semicolon or a line. Whatever still does not read\n * as hours — \"8–5\", \"Sat by appointment\" — is left out rather than guessed at.\n */\nfunction clientBuildHours(hours: string): string {\n const lines = hours\n .replace(TWELVE_HOUR_TIME, (time, hour: string, minutes: string | undefined, half: string) => {\n const h = Number(hour)\n if (h < 1 || h > 12) return time\n const clock = (h % 12) + (half.toLowerCase() === 'p' ? 12 : 0)\n return `${String(clock).padStart(2, '0')}:${minutes ?? '00'}`\n })\n .replace(/\\s+to\\s+/gi, '-')\n .replace(/(\\d{1,2}:\\d{2})\\s*,\\s*/g, '$1\\n')\n .replace(/;/g, '\\n')\n const week = weeklyHoursFromText(lines).hours\n return week.some((day) => day.length) ? weeklyHoursToText(week) : ''\n}\n\n/**\n * The towns an intake serves, one a line, as the Details step's \"Areas you\n * serve\" holds them: \"Austin, Round Rock and Pflugerville\" is three.\n */\nfunction clientBuildServiceArea(towns: string): string {\n return towns\n .split(/\\r?\\n|[,;]|\\s+(?:and|&)\\s+/i)\n .map((town) => town.trim())\n .filter(Boolean)\n .join('\\n')\n}\n\n/**\n * The questions' answers for a site staff are building for a client: what\n * the business does is the kind of site, and the details the intake holds\n * fill the Details step's own fields (AGL-3979, AGL-3995): the phone, the\n * email, the towns as the areas it serves and the hours, each held to that\n * detail's own rule, so one the Details step would refuse is left empty\n * there. Every other answer keeps its default, which the review shows to\n * change. The intake, a refused detail included, also reaches the job\n * through {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.\n */\nexport function aiSiteStartAnswersFromClientBuild(intake: ClientBuildIntake): AiSiteStartAnswers {\n const details = aiSiteDetailsOf({\n phone: intake.phone,\n email: intake.email,\n serviceArea: clientBuildServiceArea(intake.towns),\n hours: clientBuildHours(intake.hours),\n })\n return {\n ...AI_SITE_START_ANSWERS,\n siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS),\n // The site exists already and the intake answered for it: what the\n // business does picks its style, as \"Pick for me\" does, and staff change\n // it on the review like any answer.\n ...aiSiteStartStyleAnswer(SITE_STYLE_AUTO),\n phone: details.phone,\n email: details.email,\n serviceArea: details.serviceArea,\n hours: details.hours,\n }\n}\n\n/**\n * The per-site inputs the intake fills, which the guided start's own\n * questions leave empty: the business's name for the header and footer, and\n * the towns it serves as its place.\n *\n * The towns are the place only while no address is given. The Entity reads\n * `city` as the address's own locality wherever there is a street or a\n * postal code (`aiSiteEntity`), so once the answers carry either, `city` is\n * the city the Details step was given with that address, never the towns\n * (AGL-3995). The intake asks no address, so today that is the Details step's\n * own, typed on the review; the towns still reach the site as the areas it\n * serves.\n */\nexport function aiSiteStartClientInputs(\n intake: ClientBuildIntake,\n answers?: Pick<AiSiteStartAnswers, 'street' | 'postalCode' | 'city'> | null,\n): Pick<AiSiteJobInputs, 'businessName' | 'city'> {\n const addressed = Boolean(answers?.street.trim() || answers?.postalCode.trim())\n return {\n businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),\n city: bounded(addressed ? (answers?.city ?? '') : intake.towns, AI_SITE_INPUT_MAX_CHARS),\n }\n}\n\n/**\n * The intake as sentences added to the brief: how to reach the business and\n * when it is open, which the site should say where a visitor looks for them,\n * and the owner's own words, quoted, which the copy should sound like. Empty\n * answers say nothing.\n */\nexport function aiSiteStartClientBrief(intake: ClientBuildIntake): string {\n const name = intake.businessName.trim()\n const towns = intake.towns.trim()\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n const hours = intake.hours.trim()\n const words = intake.ownerWords.trim().replace(/\\s+/g, ' ')\n const sentences: string[] = []\n if (name) sentences.push(`The business is called ${name}.`)\n if (towns) sentences.push(`It serves ${towns}.`)\n const contact = [phone ? `call ${phone}` : '', email ? `email ${email}` : ''].filter(Boolean)\n if (contact.length) {\n sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`)\n }\n if (hours) sentences.push(`Its hours are ${hours}.`)\n if (words) sentences.push(`In the owner's own words: \"${words}\" Write the copy so it sounds like them.`)\n return sentences.join(' ')\n}\n\n/**\n * What the review says the client build adds beyond its own rows, so staff\n * see the intake is used without it being asked again: the business's name\n * and the owner's own words, which no row shows. The phone, email, towns and\n * hours are the review's Details rows already (AGL-3995), so the note does\n * not say them twice: \"Also from the client's details: Harbor Plumbing and\n * the owner's own words.\"\n */\nexport function aiSiteStartClientNote(intake: ClientBuildIntake): string {\n const parts = [intake.businessName.trim(), intake.ownerWords.trim() ? \"the owner's own words\" : ''].filter(Boolean)\n if (!parts.length) return 'Built from the client’s details.'\n return `Also from the client’s details: ${parts.join(' and ')}.`\n}\n"],"names":["SITE_STYLE_AUTO","weeklyHoursFromText","weeklyHoursToText","AI_SITE_DETAIL_KEYS","AI_SITE_NO_DETAILS","aiSiteDetailProblem","aiSiteDetailsOf","aiSiteKind","aiSiteKindFor","AI_SITE_INSPIRATION_INPUT","aiSiteInspirationInput","aiSiteInspirationLimit","aiSiteInspirationRefusal","aiSiteInspirationUrl","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","style","kind","pages","submissions","welcomeEmail","city","brand","AI_SITE_START_STYLE_REQUIRED","AI_SITE_START_EXTRA_KEYS","aiSiteStartExtraProblem","key","value","trim","length","code","message","aiSiteStartExtrasGiven","answers","filter","aiSiteStartStyleAnswer","aiSiteStartKind","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","Number","isInteger","min","max","includes","problem","aiSiteStartInspirationRefusal","aiSiteStartInspirationTyped","inspiration","map","Boolean","typed","some","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","details","businessType","starter","siteKind","businessName","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","fromEntries","split","slice","bounded","trimmed","replace","cut","space","lastIndexOf","TWELVE_HOUR_TIME","clientBuildHours","hours","lines","time","hour","minutes","half","h","clock","String","padStart","week","day","clientBuildServiceArea","towns","town","aiSiteStartAnswersFromClientBuild","intake","phone","email","serviceArea","whatTheyDo","aiSiteStartClientInputs","addressed","street","postalCode","aiSiteStartClientBrief","name","words","ownerWords","contact","aiSiteStartClientNote","parts"],"mappings":"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"}
|
|
@@ -64,6 +64,14 @@ export declare const ASSIST_EDIT_CONTEXT_MAX_PROPS = 16;
|
|
|
64
64
|
export declare const ASSIST_EDIT_SELECTED_TEXT_CHARS = 400;
|
|
65
65
|
/** Characters of a string prop every other element carries. */
|
|
66
66
|
export declare const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80;
|
|
67
|
+
/**
|
|
68
|
+
* Elements a whole-document outline describes at most: one the server reads
|
|
69
|
+
* from the stored document for an edit job, where nothing is selected and
|
|
70
|
+
* nobody can be asked to select what the outline leaves out. Far past a page
|
|
71
|
+
* an AI build or a starter makes (the starter home has 182), so in practice
|
|
72
|
+
* the model sees every element it could be asked about.
|
|
73
|
+
*/
|
|
74
|
+
export declare const ASSIST_EDIT_WHOLE_MAX_NODES = 400;
|
|
67
75
|
/**
|
|
68
76
|
* One element of the canvas outline a request carries. Ids, component ids
|
|
69
77
|
* and short primitive values only — never a stored document, never markup.
|
|
@@ -80,7 +88,10 @@ export interface AssistEditCanvasNode {
|
|
|
80
88
|
name?: string;
|
|
81
89
|
/** Primitive props, strings shortened. */
|
|
82
90
|
props?: Record<string, string | number | boolean>;
|
|
83
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The element's own styles — carried for the selected element only, and
|
|
93
|
+
* for every element of a whole-document outline.
|
|
94
|
+
*/
|
|
84
95
|
sx?: Record<string, unknown>;
|
|
85
96
|
/**
|
|
86
97
|
* Described by its place alone, without props: an element around the
|
|
@@ -131,6 +142,12 @@ export interface AssistEditCanvasContext {
|
|
|
131
142
|
*/
|
|
132
143
|
total?: number;
|
|
133
144
|
nodes: AssistEditCanvasNode[];
|
|
145
|
+
/**
|
|
146
|
+
* The document as stored, read on the server for a job: every element in
|
|
147
|
+
* page order up to {@link ASSIST_EDIT_WHOLE_MAX_NODES}, each with its
|
|
148
|
+
* settings and styles, and nobody to ask. Never read from a request body.
|
|
149
|
+
*/
|
|
150
|
+
whole?: true;
|
|
134
151
|
}
|
|
135
152
|
/** An element of a validated subtree, in the flat shape the canvas stores. */
|
|
136
153
|
export interface AssistEditNode {
|
|
@@ -72,6 +72,13 @@ const BESIGNER_DOCUMENT_ROUTE = /^\/[^/]+\/hosts\/[^/]+\/(screens|components|lay
|
|
|
72
72
|
/** Props described per element at most. */ export const ASSIST_EDIT_CONTEXT_MAX_PROPS = 16;
|
|
73
73
|
/** Characters of a string prop the selected element carries into a request. */ export const ASSIST_EDIT_SELECTED_TEXT_CHARS = 400;
|
|
74
74
|
/** Characters of a string prop every other element carries. */ export const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80;
|
|
75
|
+
/**
|
|
76
|
+
* Elements a whole-document outline describes at most: one the server reads
|
|
77
|
+
* from the stored document for an edit job, where nothing is selected and
|
|
78
|
+
* nobody can be asked to select what the outline leaves out. Far past a page
|
|
79
|
+
* an AI build or a starter makes (the starter home has 182), so in practice
|
|
80
|
+
* the model sees every element it could be asked about.
|
|
81
|
+
*/ export const ASSIST_EDIT_WHOLE_MAX_NODES = 400;
|
|
75
82
|
/* ------------------------------------------------------------------ *
|
|
76
83
|
* The interactions Assist may add (AGL-3603)
|
|
77
84
|
* ------------------------------------------------------------------ */ /**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/assist-edit.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\nimport type { ScreenSeoTextField } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodeInteraction } from '@aglyn/aglyn/app-utils/node-interactions'\n\n/**\n * Assist level 3, the edit contract (AGL-2906): what the chat door proposes\n * for a besigner canvas, and what the panel applies once the author says so.\n *\n * Shared by the server, which validates a model's proposal into these\n * shapes, and the client, which applies them. Pure data and pure functions,\n * so the panel imports it without pulling the palette or the runtime.\n *\n * ## The boundary\n *\n * A proposal is OPERATIONS, never a document. The server writes nothing: it\n * answers with ops that name elements already on the canvas, or a validated\n * subtree to insert, and the client applies them through the canvas's own\n * guarded mutators, on confirm, as one undoable step. Every change lands as\n * an unsaved edit on the version the editor has open — a draft — and on a\n * version the live site serves, the editor's own new-version flow runs first.\n */\n\n/** The nine operations a proposal may carry. */\nexport const ASSIST_EDIT_OP_KINDS = [\n 'insertSubtree',\n 'updateProps',\n 'updateSx',\n 'move',\n 'remove',\n 'setSeo',\n 'rename',\n 'saveAsComponent',\n 'addInteraction',\n] as const\n\nexport type AssistEditOpKind = (typeof ASSIST_EDIT_OP_KINDS)[number]\n\n/** The id of the besigner view's edit action — the one edit a view offers. */\nexport const ASSIST_EDIT_ACTION_ID = 'edit.canvas'\n\n/** The name of the structured-output tool a model proposes an edit through. */\nexport const ASSIST_EDIT_TOOL_NAME = 'propose_canvas_edit'\n\n/** The besigner documents the edit rung opens on: those that keep versions. */\nexport type AssistEditDocumentKind = 'screen' | 'component' | 'layout'\n\nexport const ASSIST_EDIT_DOCUMENT_KINDS: readonly AssistEditDocumentKind[] = [\n 'screen',\n 'component',\n 'layout',\n]\n\nconst COLLECTION_KINDS: Readonly<Record<string, AssistEditDocumentKind>> = {\n screens: 'screen',\n components: 'component',\n layouts: 'layout',\n}\n\nconst BESIGNER_DOCUMENT_ROUTE =\n /^\\/[^/]+\\/hosts\\/[^/]+\\/(screens|components|layouts)\\/([A-Za-z0-9_-]{1,64})\\/versions\\/([A-Za-z0-9_-]{1,64})\\/besigner\\/?$/\n\n/** The document a besigner route has open. */\nexport interface AssistEditDocument {\n kind: AssistEditDocumentKind\n documentId: string\n versionId: string\n}\n\n/**\n * The open document off a console path, or null off a route the edit rung\n * serves. Only the versioned editors qualify — a template or an email editor\n * has no version to make first, so the rung does not open there.\n */\nexport function assistEditDocumentOf(route: string): AssistEditDocument | null {\n const match = BESIGNER_DOCUMENT_ROUTE.exec(String(route ?? ''))\n if (!match) return null\n return {\n kind: COLLECTION_KINDS[match[1]],\n documentId: match[2],\n versionId: match[3],\n }\n}\n\n/* ------------------------------------------------------------------ *\n * What the client tells the server about the canvas\n * ------------------------------------------------------------------ */\n\n/** Elements a request describes at most — the selection, its surroundings, its subtree. */\nexport const ASSIST_EDIT_CONTEXT_MAX_NODES = 60\n/** Props described per element at most. */\nexport const ASSIST_EDIT_CONTEXT_MAX_PROPS = 16\n/** Characters of a string prop the selected element carries into a request. */\nexport const ASSIST_EDIT_SELECTED_TEXT_CHARS = 400\n/** Characters of a string prop every other element carries. */\nexport const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80\n\n/**\n * One element of the canvas outline a request carries. Ids, component ids\n * and short primitive values only — never a stored document, never markup.\n */\nexport interface AssistEditCanvasNode {\n id: string\n componentId: string\n /** Null for the document root. */\n parentId: string | null\n /** Position among the parent's children. */\n index: number\n childCount: number\n /** The layer name, when the author gave it one. */\n name?: string\n /** Primitive props, strings shortened. */\n props?: Record<string, string | number | boolean>\n /** The element's own styles — carried for the selected element only. */\n sx?: Record<string, unknown>\n /**\n * Described by its place alone, without props: an element around the\n * selection, there so the model knows it exists and can ask for it.\n */\n brief?: boolean\n /**\n * An element described earlier whose contents this one's are built like,\n * the way the cards of a row are. Its own contents are left out.\n */\n like?: string\n /**\n * The interactions the element carries (AGL-3603), each by its name, the\n * event it listens for and its steps' kinds — carried for the selected\n * element only, so Assist can say what it does. Never a selector, a script\n * or HTML.\n */\n interactions?: AssistEditInteractionSummary[]\n}\n\n/** One interaction on an element, as an outline describes it. */\nexport interface AssistEditInteractionSummary {\n name: string\n event: string\n steps: string[]\n enabled: boolean\n}\n\n/* ------------------------------------------------------------------ *\n * The interactions Assist may add (AGL-3603)\n * ------------------------------------------------------------------ */\n\n/**\n * The events an interaction Assist adds may listen for: those that watch the\n * element it is added to, the way the Besigner's Interactions section offers\n * them on a selected element.\n */\nexport const ASSIST_EDIT_INTERACTION_EVENTS = [\n 'elementClick',\n 'elementHoverEnter',\n 'elementHoverLeave',\n 'elementVisible',\n 'scrollToElement',\n] as const\n\nexport type AssistEditInteractionEvent = (typeof ASSIST_EDIT_INTERACTION_EVENTS)[number]\n\n/**\n * The steps it may take: the platform's basic presentational client steps,\n * which run on every plan and touch no data. No script, no HTML, no\n * attribute, no overlay, no analytics event — and no server step.\n */\nexport const ASSIST_EDIT_INTERACTION_STEPS = [\n 'showElement',\n 'hideElement',\n 'toggleElement',\n 'addClass',\n 'removeClass',\n 'toggleClass',\n 'scrollTo',\n 'playVideo',\n 'openDrawer',\n 'closeDrawer',\n 'toggleDrawer',\n 'openMenu',\n 'closeMenu',\n 'toggleMenu',\n 'redirect',\n 'siteAlert',\n] as const\n\nexport type AssistEditInteractionStep = (typeof ASSIST_EDIT_INTERACTION_STEPS)[number]\n\nexport interface AssistEditCanvasContext {\n /** The element the author has selected, or null. */\n selectedId: string | null\n /**\n * Elements in the whole document, the root included, so the prompt can say\n * how much of it `nodes` leaves out. Absent from an outline sent by a panel\n * that predates it.\n */\n total?: number\n nodes: AssistEditCanvasNode[]\n}\n\n/* ------------------------------------------------------------------ *\n * What the server proposes\n * ------------------------------------------------------------------ */\n\n/** An element of a validated subtree, in the flat shape the canvas stores. */\nexport interface AssistEditNode {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n nodes: string[]\n props: Record<string, unknown>\n sx?: Record<string, unknown>\n hidden?: boolean\n}\n\n/**\n * Every op that acts on an element names the component the server validated\n * it as, so the client can refuse an element that has changed since.\n */\ninterface AssistEditNodeOpBase {\n nodeId: string\n componentId: string\n}\n\nexport interface AssistEditInsertOp {\n op: 'insertSubtree'\n parentId: string\n /** The component the parent was validated as. */\n parentComponentId: string\n /** Position among the parent's children; null appends. */\n index: number | null\n rootId: string\n nodes: Record<string, AssistEditNode>\n}\n\nexport interface AssistEditUpdatePropsOp extends AssistEditNodeOpBase {\n op: 'updateProps'\n /** Merged over the props the element has; nothing else it carries changes. */\n props: Record<string, unknown>\n}\n\nexport interface AssistEditUpdateSxOp extends AssistEditNodeOpBase {\n op: 'updateSx'\n /** Merged over the element's own styles, one key at a time. */\n sx: Record<string, unknown>\n}\n\nexport interface AssistEditMoveOp extends AssistEditNodeOpBase {\n op: 'move'\n parentId: string\n /** The component the new parent was validated as. */\n parentComponentId: string\n index: number | null\n}\n\nexport interface AssistEditRemoveOp extends AssistEditNodeOpBase {\n op: 'remove'\n}\n\nexport interface AssistEditRenameOp extends AssistEditNodeOpBase {\n op: 'rename'\n name: string\n}\n\nexport interface AssistEditSetSeoOp {\n op: 'setSeo'\n fields: Partial<Record<ScreenSeoTextField, string>>\n}\n\n/**\n * A property the promoted subtree declares (AGL-2908), in the shape the\n * Properties dialog stores one — without a default.\n *\n * The server never sees a full value: the outline it read the selection from\n * shortens every string. So the default is read off the live canvas at apply\n * time, from the very field the token is about to replace, which is what\n * makes the instance render what was there a moment before.\n */\nexport type AssistEditComponentProp = Omit<ReusableComponentProp, 'defaultValue'>\n\n/** Where one property's token goes: an element of the selection, and a field of it. */\nexport interface AssistEditPropBinding {\n nodeId: string\n /** The component the element was validated as, so a changed one is refused. */\n componentId: string\n /** A prop of that element, or `hideIf` for a part a Yes / no hides. */\n field: string\n /** The property whose `{{prop.<name>}}` token the field takes. */\n prop: string\n}\n\n/**\n * Save the selection as a reusable component (AGL-2908), with the properties\n * the model proposed and the tokens bound into the tree.\n *\n * The only op that creates a document. It creates exactly the one the manual\n * Save as reusable component creates, through the same host resources route,\n * where the plan's entitlement and the route's allow-list are enforced — and\n * it changes the open canvas the same way that action does, by swapping the\n * promoted subtree for an instance. Nothing is published and nothing is\n * saved: the swap is an unsaved edit on the version the editor has open.\n */\nexport interface AssistEditSaveAsComponentOp extends AssistEditNodeOpBase {\n op: 'saveAsComponent'\n /** The name the component is created under. */\n name: string\n props: AssistEditComponentProp[]\n bindings: AssistEditPropBinding[]\n}\n\n/**\n * Add one interaction to an element (AGL-3603), in the shape the element\n * stores it — its trigger with no selector, which the element's id derives.\n * It never replaces or removes one the element already carries.\n */\nexport interface AssistEditAddInteractionOp extends AssistEditNodeOpBase {\n op: 'addInteraction'\n interaction: NodeInteraction\n /** Every other element a step acts on, each with the component it was validated as. */\n targets: Array<{ nodeId: string; componentId: string }>\n}\n\nexport type AssistEditOp =\n | AssistEditAddInteractionOp\n | AssistEditInsertOp\n | AssistEditUpdatePropsOp\n | AssistEditUpdateSxOp\n | AssistEditMoveOp\n | AssistEditRemoveOp\n | AssistEditRenameOp\n | AssistEditSetSeoOp\n | AssistEditSaveAsComponentOp\n\n/** Where a proposal applies, as the server read it off the request. */\nexport interface AssistEditTarget extends AssistEditDocument {\n hostId: string\n}\n\n/** The counts the card shows before the author decides. */\nexport interface AssistEditDiff {\n /** Elements added, every element of every inserted subtree counted. */\n added: number\n /** Elements removed — each with whatever it contains. */\n removed: number\n propsChanged: number\n stylesChanged: number\n moved: number\n renamed: number\n /** Search fields filled in. */\n seoFields: number\n /** Properties the selection declares once it is saved as a component. */\n componentProps: number\n /** Selections saved as a reusable component — at most one per proposal. */\n componentsSaved: number\n /** Interactions added to elements. */\n interactionsAdded: number\n}\n\n/** The most reasons a proposal carries for the changes validation left out. */\nexport const ASSIST_EDIT_MAX_DROPPED_REASONS = 10\n\nexport interface AssistEditProposal {\n id: typeof ASSIST_EDIT_ACTION_ID\n /** One line, in the model's words, of what the edit does. */\n summary: string\n ops: AssistEditOp[]\n diff: AssistEditDiff\n /** Why each change the model wrote and validation refused was left out. */\n dropped: string[]\n target: AssistEditTarget\n}\n\n/** The counts for a list of ops. */\nexport function summarizeAssistEditOps(ops: readonly AssistEditOp[]): AssistEditDiff {\n const diff: AssistEditDiff = {\n added: 0,\n removed: 0,\n propsChanged: 0,\n stylesChanged: 0,\n moved: 0,\n renamed: 0,\n seoFields: 0,\n componentProps: 0,\n componentsSaved: 0,\n interactionsAdded: 0,\n }\n for (const op of ops) {\n switch (op.op) {\n case 'insertSubtree':\n diff.added += Object.keys(op.nodes).length\n break\n case 'remove':\n diff.removed += 1\n break\n case 'updateProps':\n diff.propsChanged += 1\n break\n case 'updateSx':\n diff.stylesChanged += 1\n break\n case 'move':\n diff.moved += 1\n break\n case 'rename':\n diff.renamed += 1\n break\n case 'setSeo':\n diff.seoFields += Object.keys(op.fields).length\n break\n case 'saveAsComponent':\n diff.componentsSaved += 1\n diff.componentProps += op.props.length\n break\n case 'addInteraction':\n diff.interactionsAdded += 1\n break\n }\n }\n return diff\n}\n\nconst plural = (count: number, one: string, many: string): string =>\n `${count} ${count === 1 ? one : many}`\n\n/** The diff as the lines a person reads, zero counts omitted. */\nexport function describeAssistEditDiff(diff: AssistEditDiff): string[] {\n const lines: string[] = []\n if (diff.added) lines.push(`${plural(diff.added, 'element', 'elements')} added`)\n if (diff.removed) lines.push(`${plural(diff.removed, 'element', 'elements')} removed`)\n if (diff.propsChanged) {\n lines.push(`${plural(diff.propsChanged, 'element’s settings', 'elements’ settings')} changed`)\n }\n if (diff.stylesChanged) {\n lines.push(`${plural(diff.stylesChanged, 'element', 'elements')} restyled`)\n }\n if (diff.moved) lines.push(`${plural(diff.moved, 'element', 'elements')} moved`)\n if (diff.renamed) lines.push(`${plural(diff.renamed, 'layer', 'layers')} renamed`)\n if (diff.seoFields) {\n lines.push(`${plural(diff.seoFields, 'search field', 'search fields')} filled in`)\n }\n if (diff.interactionsAdded) {\n lines.push(`${plural(diff.interactionsAdded, 'interaction', 'interactions')} added`)\n }\n if (diff.componentsSaved) {\n lines.push(\n `saved as a reusable component with ${plural(diff.componentProps, 'property', 'properties')}`,\n )\n }\n return lines\n}\n\n/** The word each op kind is counted under in the activity row. */\nconst OP_COUNT_WORDS: Readonly<Record<AssistEditOpKind, string>> = {\n updateProps: 'set',\n updateSx: 'restyle',\n insertSubtree: 'insert',\n move: 'move',\n remove: 'remove',\n rename: 'rename',\n setSeo: 'seo',\n saveAsComponent: 'component',\n addInteraction: 'interaction',\n}\n\n/** The applied ops counted by kind, as the activity row names them (`{ set: 3, insert: 1 }`). */\nexport function assistEditOpCounts(ops: readonly AssistEditOp[]): Record<string, number> {\n const counts: Record<string, number> = {}\n for (const op of ops) {\n const word = OP_COUNT_WORDS[op.op]\n if (word) counts[word] = (counts[word] ?? 0) + 1\n }\n return counts\n}\n\n/** The words `assistEditOpCounts` counts under, for a door that reads them back. */\nexport const ASSIST_EDIT_OP_COUNT_WORDS: readonly string[] = Object.values(OP_COUNT_WORDS)\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Whether a `done` event's `edit` field is a proposal this client can apply.\n * The server built it, so this is a shape check rather than a validation: it\n * keeps a malformed or foreign payload from reaching the canvas mutators.\n */\nexport function isAssistEditProposal(value: unknown): value is AssistEditProposal {\n if (!isRecord(value) || value['id'] !== ASSIST_EDIT_ACTION_ID) return false\n const target = value['target']\n if (!isRecord(target)) return false\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(target['kind'] as AssistEditDocumentKind)) return false\n if (typeof target['documentId'] !== 'string' || !target['documentId']) return false\n const ops = value['ops']\n if (!Array.isArray(ops) || !ops.length) return false\n return ops.every(\n (op) => isRecord(op) && ASSIST_EDIT_OP_KINDS.includes(op['op'] as AssistEditOpKind),\n )\n}\n\n/* ------------------------------------------------------------------ *\n * What the client reports once the author applied a proposal\n * ------------------------------------------------------------------ */\n\n/** The body of `POST /api/assist/edit-applied`. */\nexport interface AssistEditAppliedReport {\n orgId: string\n hostId: string\n /** The exchange whose answer carried the proposal. */\n exchangeId: string\n documentKind: AssistEditDocumentKind\n documentId: string\n /** The version the edits landed on — a new one when the open one was live. */\n versionId: string\n opCounts: Record<string, number>\n}\n"],"names":["ASSIST_EDIT_OP_KINDS","ASSIST_EDIT_ACTION_ID","ASSIST_EDIT_TOOL_NAME","ASSIST_EDIT_DOCUMENT_KINDS","COLLECTION_KINDS","screens","components","layouts","BESIGNER_DOCUMENT_ROUTE","assistEditDocumentOf","route","match","exec","String","kind","documentId","versionId","ASSIST_EDIT_CONTEXT_MAX_NODES","ASSIST_EDIT_CONTEXT_MAX_PROPS","ASSIST_EDIT_SELECTED_TEXT_CHARS","ASSIST_EDIT_OUTLINE_TEXT_CHARS","ASSIST_EDIT_INTERACTION_EVENTS","ASSIST_EDIT_INTERACTION_STEPS","ASSIST_EDIT_MAX_DROPPED_REASONS","summarizeAssistEditOps","ops","diff","added","removed","propsChanged","stylesChanged","moved","renamed","seoFields","componentProps","componentsSaved","interactionsAdded","op","Object","keys","nodes","length","fields","props","plural","count","one","many","describeAssistEditDiff","lines","push","OP_COUNT_WORDS","updateProps","updateSx","insertSubtree","move","remove","rename","setSeo","saveAsComponent","addInteraction","assistEditOpCounts","counts","word","ASSIST_EDIT_OP_COUNT_WORDS","values","isRecord","value","Array","isArray","isAssistEditProposal","target","includes","every"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/assist-edit.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\nimport type { ScreenSeoTextField } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodeInteraction } from '@aglyn/aglyn/app-utils/node-interactions'\n\n/**\n * Assist level 3, the edit contract (AGL-2906): what the chat door proposes\n * for a besigner canvas, and what the panel applies once the author says so.\n *\n * Shared by the server, which validates a model's proposal into these\n * shapes, and the client, which applies them. Pure data and pure functions,\n * so the panel imports it without pulling the palette or the runtime.\n *\n * ## The boundary\n *\n * A proposal is OPERATIONS, never a document. The server writes nothing: it\n * answers with ops that name elements already on the canvas, or a validated\n * subtree to insert, and the client applies them through the canvas's own\n * guarded mutators, on confirm, as one undoable step. Every change lands as\n * an unsaved edit on the version the editor has open — a draft — and on a\n * version the live site serves, the editor's own new-version flow runs first.\n */\n\n/** The nine operations a proposal may carry. */\nexport const ASSIST_EDIT_OP_KINDS = [\n 'insertSubtree',\n 'updateProps',\n 'updateSx',\n 'move',\n 'remove',\n 'setSeo',\n 'rename',\n 'saveAsComponent',\n 'addInteraction',\n] as const\n\nexport type AssistEditOpKind = (typeof ASSIST_EDIT_OP_KINDS)[number]\n\n/** The id of the besigner view's edit action — the one edit a view offers. */\nexport const ASSIST_EDIT_ACTION_ID = 'edit.canvas'\n\n/** The name of the structured-output tool a model proposes an edit through. */\nexport const ASSIST_EDIT_TOOL_NAME = 'propose_canvas_edit'\n\n/** The besigner documents the edit rung opens on: those that keep versions. */\nexport type AssistEditDocumentKind = 'screen' | 'component' | 'layout'\n\nexport const ASSIST_EDIT_DOCUMENT_KINDS: readonly AssistEditDocumentKind[] = [\n 'screen',\n 'component',\n 'layout',\n]\n\nconst COLLECTION_KINDS: Readonly<Record<string, AssistEditDocumentKind>> = {\n screens: 'screen',\n components: 'component',\n layouts: 'layout',\n}\n\nconst BESIGNER_DOCUMENT_ROUTE =\n /^\\/[^/]+\\/hosts\\/[^/]+\\/(screens|components|layouts)\\/([A-Za-z0-9_-]{1,64})\\/versions\\/([A-Za-z0-9_-]{1,64})\\/besigner\\/?$/\n\n/** The document a besigner route has open. */\nexport interface AssistEditDocument {\n kind: AssistEditDocumentKind\n documentId: string\n versionId: string\n}\n\n/**\n * The open document off a console path, or null off a route the edit rung\n * serves. Only the versioned editors qualify — a template or an email editor\n * has no version to make first, so the rung does not open there.\n */\nexport function assistEditDocumentOf(route: string): AssistEditDocument | null {\n const match = BESIGNER_DOCUMENT_ROUTE.exec(String(route ?? ''))\n if (!match) return null\n return {\n kind: COLLECTION_KINDS[match[1]],\n documentId: match[2],\n versionId: match[3],\n }\n}\n\n/* ------------------------------------------------------------------ *\n * What the client tells the server about the canvas\n * ------------------------------------------------------------------ */\n\n/** Elements a request describes at most — the selection, its surroundings, its subtree. */\nexport const ASSIST_EDIT_CONTEXT_MAX_NODES = 60\n/** Props described per element at most. */\nexport const ASSIST_EDIT_CONTEXT_MAX_PROPS = 16\n/** Characters of a string prop the selected element carries into a request. */\nexport const ASSIST_EDIT_SELECTED_TEXT_CHARS = 400\n/** Characters of a string prop every other element carries. */\nexport const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80\n/**\n * Elements a whole-document outline describes at most: one the server reads\n * from the stored document for an edit job, where nothing is selected and\n * nobody can be asked to select what the outline leaves out. Far past a page\n * an AI build or a starter makes (the starter home has 182), so in practice\n * the model sees every element it could be asked about.\n */\nexport const ASSIST_EDIT_WHOLE_MAX_NODES = 400\n\n/**\n * One element of the canvas outline a request carries. Ids, component ids\n * and short primitive values only — never a stored document, never markup.\n */\nexport interface AssistEditCanvasNode {\n id: string\n componentId: string\n /** Null for the document root. */\n parentId: string | null\n /** Position among the parent's children. */\n index: number\n childCount: number\n /** The layer name, when the author gave it one. */\n name?: string\n /** Primitive props, strings shortened. */\n props?: Record<string, string | number | boolean>\n /**\n * The element's own styles — carried for the selected element only, and\n * for every element of a whole-document outline.\n */\n sx?: Record<string, unknown>\n /**\n * Described by its place alone, without props: an element around the\n * selection, there so the model knows it exists and can ask for it.\n */\n brief?: boolean\n /**\n * An element described earlier whose contents this one's are built like,\n * the way the cards of a row are. Its own contents are left out.\n */\n like?: string\n /**\n * The interactions the element carries (AGL-3603), each by its name, the\n * event it listens for and its steps' kinds — carried for the selected\n * element only, so Assist can say what it does. Never a selector, a script\n * or HTML.\n */\n interactions?: AssistEditInteractionSummary[]\n}\n\n/** One interaction on an element, as an outline describes it. */\nexport interface AssistEditInteractionSummary {\n name: string\n event: string\n steps: string[]\n enabled: boolean\n}\n\n/* ------------------------------------------------------------------ *\n * The interactions Assist may add (AGL-3603)\n * ------------------------------------------------------------------ */\n\n/**\n * The events an interaction Assist adds may listen for: those that watch the\n * element it is added to, the way the Besigner's Interactions section offers\n * them on a selected element.\n */\nexport const ASSIST_EDIT_INTERACTION_EVENTS = [\n 'elementClick',\n 'elementHoverEnter',\n 'elementHoverLeave',\n 'elementVisible',\n 'scrollToElement',\n] as const\n\nexport type AssistEditInteractionEvent = (typeof ASSIST_EDIT_INTERACTION_EVENTS)[number]\n\n/**\n * The steps it may take: the platform's basic presentational client steps,\n * which run on every plan and touch no data. No script, no HTML, no\n * attribute, no overlay, no analytics event — and no server step.\n */\nexport const ASSIST_EDIT_INTERACTION_STEPS = [\n 'showElement',\n 'hideElement',\n 'toggleElement',\n 'addClass',\n 'removeClass',\n 'toggleClass',\n 'scrollTo',\n 'playVideo',\n 'openDrawer',\n 'closeDrawer',\n 'toggleDrawer',\n 'openMenu',\n 'closeMenu',\n 'toggleMenu',\n 'redirect',\n 'siteAlert',\n] as const\n\nexport type AssistEditInteractionStep = (typeof ASSIST_EDIT_INTERACTION_STEPS)[number]\n\nexport interface AssistEditCanvasContext {\n /** The element the author has selected, or null. */\n selectedId: string | null\n /**\n * Elements in the whole document, the root included, so the prompt can say\n * how much of it `nodes` leaves out. Absent from an outline sent by a panel\n * that predates it.\n */\n total?: number\n nodes: AssistEditCanvasNode[]\n /**\n * The document as stored, read on the server for a job: every element in\n * page order up to {@link ASSIST_EDIT_WHOLE_MAX_NODES}, each with its\n * settings and styles, and nobody to ask. Never read from a request body.\n */\n whole?: true\n}\n\n/* ------------------------------------------------------------------ *\n * What the server proposes\n * ------------------------------------------------------------------ */\n\n/** An element of a validated subtree, in the flat shape the canvas stores. */\nexport interface AssistEditNode {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n nodes: string[]\n props: Record<string, unknown>\n sx?: Record<string, unknown>\n hidden?: boolean\n}\n\n/**\n * Every op that acts on an element names the component the server validated\n * it as, so the client can refuse an element that has changed since.\n */\ninterface AssistEditNodeOpBase {\n nodeId: string\n componentId: string\n}\n\nexport interface AssistEditInsertOp {\n op: 'insertSubtree'\n parentId: string\n /** The component the parent was validated as. */\n parentComponentId: string\n /** Position among the parent's children; null appends. */\n index: number | null\n rootId: string\n nodes: Record<string, AssistEditNode>\n}\n\nexport interface AssistEditUpdatePropsOp extends AssistEditNodeOpBase {\n op: 'updateProps'\n /** Merged over the props the element has; nothing else it carries changes. */\n props: Record<string, unknown>\n}\n\nexport interface AssistEditUpdateSxOp extends AssistEditNodeOpBase {\n op: 'updateSx'\n /** Merged over the element's own styles, one key at a time. */\n sx: Record<string, unknown>\n}\n\nexport interface AssistEditMoveOp extends AssistEditNodeOpBase {\n op: 'move'\n parentId: string\n /** The component the new parent was validated as. */\n parentComponentId: string\n index: number | null\n}\n\nexport interface AssistEditRemoveOp extends AssistEditNodeOpBase {\n op: 'remove'\n}\n\nexport interface AssistEditRenameOp extends AssistEditNodeOpBase {\n op: 'rename'\n name: string\n}\n\nexport interface AssistEditSetSeoOp {\n op: 'setSeo'\n fields: Partial<Record<ScreenSeoTextField, string>>\n}\n\n/**\n * A property the promoted subtree declares (AGL-2908), in the shape the\n * Properties dialog stores one — without a default.\n *\n * The server never sees a full value: the outline it read the selection from\n * shortens every string. So the default is read off the live canvas at apply\n * time, from the very field the token is about to replace, which is what\n * makes the instance render what was there a moment before.\n */\nexport type AssistEditComponentProp = Omit<ReusableComponentProp, 'defaultValue'>\n\n/** Where one property's token goes: an element of the selection, and a field of it. */\nexport interface AssistEditPropBinding {\n nodeId: string\n /** The component the element was validated as, so a changed one is refused. */\n componentId: string\n /** A prop of that element, or `hideIf` for a part a Yes / no hides. */\n field: string\n /** The property whose `{{prop.<name>}}` token the field takes. */\n prop: string\n}\n\n/**\n * Save the selection as a reusable component (AGL-2908), with the properties\n * the model proposed and the tokens bound into the tree.\n *\n * The only op that creates a document. It creates exactly the one the manual\n * Save as reusable component creates, through the same host resources route,\n * where the plan's entitlement and the route's allow-list are enforced — and\n * it changes the open canvas the same way that action does, by swapping the\n * promoted subtree for an instance. Nothing is published and nothing is\n * saved: the swap is an unsaved edit on the version the editor has open.\n */\nexport interface AssistEditSaveAsComponentOp extends AssistEditNodeOpBase {\n op: 'saveAsComponent'\n /** The name the component is created under. */\n name: string\n props: AssistEditComponentProp[]\n bindings: AssistEditPropBinding[]\n}\n\n/**\n * Add one interaction to an element (AGL-3603), in the shape the element\n * stores it — its trigger with no selector, which the element's id derives.\n * It never replaces or removes one the element already carries.\n */\nexport interface AssistEditAddInteractionOp extends AssistEditNodeOpBase {\n op: 'addInteraction'\n interaction: NodeInteraction\n /** Every other element a step acts on, each with the component it was validated as. */\n targets: Array<{ nodeId: string; componentId: string }>\n}\n\nexport type AssistEditOp =\n | AssistEditAddInteractionOp\n | AssistEditInsertOp\n | AssistEditUpdatePropsOp\n | AssistEditUpdateSxOp\n | AssistEditMoveOp\n | AssistEditRemoveOp\n | AssistEditRenameOp\n | AssistEditSetSeoOp\n | AssistEditSaveAsComponentOp\n\n/** Where a proposal applies, as the server read it off the request. */\nexport interface AssistEditTarget extends AssistEditDocument {\n hostId: string\n}\n\n/** The counts the card shows before the author decides. */\nexport interface AssistEditDiff {\n /** Elements added, every element of every inserted subtree counted. */\n added: number\n /** Elements removed — each with whatever it contains. */\n removed: number\n propsChanged: number\n stylesChanged: number\n moved: number\n renamed: number\n /** Search fields filled in. */\n seoFields: number\n /** Properties the selection declares once it is saved as a component. */\n componentProps: number\n /** Selections saved as a reusable component — at most one per proposal. */\n componentsSaved: number\n /** Interactions added to elements. */\n interactionsAdded: number\n}\n\n/** The most reasons a proposal carries for the changes validation left out. */\nexport const ASSIST_EDIT_MAX_DROPPED_REASONS = 10\n\nexport interface AssistEditProposal {\n id: typeof ASSIST_EDIT_ACTION_ID\n /** One line, in the model's words, of what the edit does. */\n summary: string\n ops: AssistEditOp[]\n diff: AssistEditDiff\n /** Why each change the model wrote and validation refused was left out. */\n dropped: string[]\n target: AssistEditTarget\n}\n\n/** The counts for a list of ops. */\nexport function summarizeAssistEditOps(ops: readonly AssistEditOp[]): AssistEditDiff {\n const diff: AssistEditDiff = {\n added: 0,\n removed: 0,\n propsChanged: 0,\n stylesChanged: 0,\n moved: 0,\n renamed: 0,\n seoFields: 0,\n componentProps: 0,\n componentsSaved: 0,\n interactionsAdded: 0,\n }\n for (const op of ops) {\n switch (op.op) {\n case 'insertSubtree':\n diff.added += Object.keys(op.nodes).length\n break\n case 'remove':\n diff.removed += 1\n break\n case 'updateProps':\n diff.propsChanged += 1\n break\n case 'updateSx':\n diff.stylesChanged += 1\n break\n case 'move':\n diff.moved += 1\n break\n case 'rename':\n diff.renamed += 1\n break\n case 'setSeo':\n diff.seoFields += Object.keys(op.fields).length\n break\n case 'saveAsComponent':\n diff.componentsSaved += 1\n diff.componentProps += op.props.length\n break\n case 'addInteraction':\n diff.interactionsAdded += 1\n break\n }\n }\n return diff\n}\n\nconst plural = (count: number, one: string, many: string): string =>\n `${count} ${count === 1 ? one : many}`\n\n/** The diff as the lines a person reads, zero counts omitted. */\nexport function describeAssistEditDiff(diff: AssistEditDiff): string[] {\n const lines: string[] = []\n if (diff.added) lines.push(`${plural(diff.added, 'element', 'elements')} added`)\n if (diff.removed) lines.push(`${plural(diff.removed, 'element', 'elements')} removed`)\n if (diff.propsChanged) {\n lines.push(`${plural(diff.propsChanged, 'element’s settings', 'elements’ settings')} changed`)\n }\n if (diff.stylesChanged) {\n lines.push(`${plural(diff.stylesChanged, 'element', 'elements')} restyled`)\n }\n if (diff.moved) lines.push(`${plural(diff.moved, 'element', 'elements')} moved`)\n if (diff.renamed) lines.push(`${plural(diff.renamed, 'layer', 'layers')} renamed`)\n if (diff.seoFields) {\n lines.push(`${plural(diff.seoFields, 'search field', 'search fields')} filled in`)\n }\n if (diff.interactionsAdded) {\n lines.push(`${plural(diff.interactionsAdded, 'interaction', 'interactions')} added`)\n }\n if (diff.componentsSaved) {\n lines.push(\n `saved as a reusable component with ${plural(diff.componentProps, 'property', 'properties')}`,\n )\n }\n return lines\n}\n\n/** The word each op kind is counted under in the activity row. */\nconst OP_COUNT_WORDS: Readonly<Record<AssistEditOpKind, string>> = {\n updateProps: 'set',\n updateSx: 'restyle',\n insertSubtree: 'insert',\n move: 'move',\n remove: 'remove',\n rename: 'rename',\n setSeo: 'seo',\n saveAsComponent: 'component',\n addInteraction: 'interaction',\n}\n\n/** The applied ops counted by kind, as the activity row names them (`{ set: 3, insert: 1 }`). */\nexport function assistEditOpCounts(ops: readonly AssistEditOp[]): Record<string, number> {\n const counts: Record<string, number> = {}\n for (const op of ops) {\n const word = OP_COUNT_WORDS[op.op]\n if (word) counts[word] = (counts[word] ?? 0) + 1\n }\n return counts\n}\n\n/** The words `assistEditOpCounts` counts under, for a door that reads them back. */\nexport const ASSIST_EDIT_OP_COUNT_WORDS: readonly string[] = Object.values(OP_COUNT_WORDS)\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Whether a `done` event's `edit` field is a proposal this client can apply.\n * The server built it, so this is a shape check rather than a validation: it\n * keeps a malformed or foreign payload from reaching the canvas mutators.\n */\nexport function isAssistEditProposal(value: unknown): value is AssistEditProposal {\n if (!isRecord(value) || value['id'] !== ASSIST_EDIT_ACTION_ID) return false\n const target = value['target']\n if (!isRecord(target)) return false\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(target['kind'] as AssistEditDocumentKind)) return false\n if (typeof target['documentId'] !== 'string' || !target['documentId']) return false\n const ops = value['ops']\n if (!Array.isArray(ops) || !ops.length) return false\n return ops.every(\n (op) => isRecord(op) && ASSIST_EDIT_OP_KINDS.includes(op['op'] as AssistEditOpKind),\n )\n}\n\n/* ------------------------------------------------------------------ *\n * What the client reports once the author applied a proposal\n * ------------------------------------------------------------------ */\n\n/** The body of `POST /api/assist/edit-applied`. */\nexport interface AssistEditAppliedReport {\n orgId: string\n hostId: string\n /** The exchange whose answer carried the proposal. */\n exchangeId: string\n documentKind: AssistEditDocumentKind\n documentId: string\n /** The version the edits landed on — a new one when the open one was live. */\n versionId: string\n opCounts: Record<string, number>\n}\n"],"names":["ASSIST_EDIT_OP_KINDS","ASSIST_EDIT_ACTION_ID","ASSIST_EDIT_TOOL_NAME","ASSIST_EDIT_DOCUMENT_KINDS","COLLECTION_KINDS","screens","components","layouts","BESIGNER_DOCUMENT_ROUTE","assistEditDocumentOf","route","match","exec","String","kind","documentId","versionId","ASSIST_EDIT_CONTEXT_MAX_NODES","ASSIST_EDIT_CONTEXT_MAX_PROPS","ASSIST_EDIT_SELECTED_TEXT_CHARS","ASSIST_EDIT_OUTLINE_TEXT_CHARS","ASSIST_EDIT_WHOLE_MAX_NODES","ASSIST_EDIT_INTERACTION_EVENTS","ASSIST_EDIT_INTERACTION_STEPS","ASSIST_EDIT_MAX_DROPPED_REASONS","summarizeAssistEditOps","ops","diff","added","removed","propsChanged","stylesChanged","moved","renamed","seoFields","componentProps","componentsSaved","interactionsAdded","op","Object","keys","nodes","length","fields","props","plural","count","one","many","describeAssistEditDiff","lines","push","OP_COUNT_WORDS","updateProps","updateSx","insertSubtree","move","remove","rename","setSeo","saveAsComponent","addInteraction","assistEditOpCounts","counts","word","ASSIST_EDIT_OP_COUNT_WORDS","values","isRecord","value","Array","isArray","isAssistEditProposal","target","includes","every"],"mappings":"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"}
|