@aglyn/plugins-ai 1.0.0-beta.296 → 1.0.0
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 +2 -1
- package/src/lib/components/ai-job-links.js +8 -2
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/assist-edit-canvas.d.ts +12 -6
- package/src/lib/components/assist-edit-canvas.js +86 -36
- package/src/lib/components/assist-edit-canvas.js.map +1 -1
- package/src/lib/components/assist-edit-card.component.js +9 -8
- package/src/lib/components/assist-edit-card.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +1 -1
- package/src/lib/components/assist-page-change-card.component.js +2 -2
- package/src/lib/components/assist-page-change-card.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +19 -8
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +20 -0
- package/src/lib/jobs/ai-job-edit-step.js +39 -3
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/model/ai-edit-job.js +1 -1
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-notice.js +3 -1
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +8 -7
- package/src/lib/model/assist-edit.js +4 -4
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +14 -0
- package/src/lib/model/assist-page-change.js +21 -0
- package/src/lib/model/assist-page-change.js.map +1 -1
- package/src/lib/server/assist-chat.js +5 -1
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +12 -7
- package/src/lib/server/assist-edit.js +43 -22
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.js +1 -1
- package/src/lib/server/assist-page-change.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n /** What a job waiting in `needs_input` asks the person (AGL-3978). */\n input?: { message: string } | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n // A site import's too (AGL-3976): its waves are reviewed and started there.\n if ((job.kind === 'site' || job.kind === 'import') && job.hostId) {\n return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n }\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\n/** The site a job worked on, as its notice names it: its name, and where it is live. */\nexport interface AiJobNoticeSite {\n name: string\n /** The site's live address, absolute; `null` when it has none yet. */\n url: string | null\n}\n\nexport function aiJobNotice(\n job: AiJobNoticeSource,\n to: AiJobNoticeTransition,\n site: AiJobNoticeSite | null = null,\n): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n // A plan that needs an ID only the person has first (AGL-3978).\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : noun} needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs.`,\n link,\n ...scope,\n }\n }\n // A site import's wave is in, or its site is read (AGL-3976): a decision\n // waiting, said as one, never as a job that stopped.\n if (job.review?.reason === 'wave') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'info',\n title: 'Your site import is ready for you',\n body: job.review.message || 'Review the pages brought in, then start the next wave when you are ready.',\n link,\n ...scope,\n }\n }\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const published = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (published > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n // Named, with its address (AGL-3889): someone with several sites, or\n // reading the email days later, cannot tell which site went live from\n // \"Your site is live\", nor where to see it.\n const live = site ? `${site.name} is live` : 'Your site is live'\n const at = site?.url ? ` at ${site.url}` : ''\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? `${live}, but part of it wasn’t built` : live,\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}`\n : drafts > 0\n ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.`\n : at\n ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n\n/**\n * The one reminder a plan that waits for its confirmation gets (AGL-4076),\n * a day after it was ready. The same kind of notice as the first one\n * (`content.aiJobNeedsYou`), so the same switch in the person's notification\n * settings turns both off, and it opens the same place: AI jobs on the job, or\n * a site start's build page. It says it is the only one, because it is.\n */\nexport function aiJobPlanReminderNotice(job: AiJobNoticeSource, site: AiJobNoticeSite | null = null): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, 'needs-review')\n const on = site ? ` for ${site.name}` : ''\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun}${on} still needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs. This is the only reminder.`,\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: noun === 'AI job' ? `Your plan${on} is still waiting for you` : `Your ${noun} plan${on} is still waiting for you`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs. This is the only reminder.',\n link,\n ...scope,\n }\n}\n"],"names":["aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","site","scope","orgId","link","input","message","type","level","title","body","review","reason","error","page","Boolean","join","items","unbuilt","some","row","partial","published","sitePublish","drafts","stayed","live","name","at","url","pages","aiJobPlanReminderNotice","on"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { besignerOpenDraftHref } from '@aglyn/aglyn/app-utils/version-draft'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n /** What a job waiting in `needs_input` asks the person (AGL-3978). */\n input?: { message: string } | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n // A site import's too (AGL-3976): its waves are reviewed and started there.\n if ((job.kind === 'site' || job.kind === 'import') && job.hostId) {\n return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n }\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n const besigner = `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n return versioned.inDraft ? besignerOpenDraftHref(besigner) : besigner\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\n/** The site a job worked on, as its notice names it: its name, and where it is live. */\nexport interface AiJobNoticeSite {\n name: string\n /** The site's live address, absolute; `null` when it has none yet. */\n url: string | null\n}\n\nexport function aiJobNotice(\n job: AiJobNoticeSource,\n to: AiJobNoticeTransition,\n site: AiJobNoticeSite | null = null,\n): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n // A plan that needs an ID only the person has first (AGL-3978).\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : noun} needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs.`,\n link,\n ...scope,\n }\n }\n // A site import's wave is in, or its site is read (AGL-3976): a decision\n // waiting, said as one, never as a job that stopped.\n if (job.review?.reason === 'wave') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'info',\n title: 'Your site import is ready for you',\n body: job.review.message || 'Review the pages brought in, then start the next wave when you are ready.',\n link,\n ...scope,\n }\n }\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const published = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (published > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n // Named, with its address (AGL-3889): someone with several sites, or\n // reading the email days later, cannot tell which site went live from\n // \"Your site is live\", nor where to see it.\n const live = site ? `${site.name} is live` : 'Your site is live'\n const at = site?.url ? ` at ${site.url}` : ''\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? `${live}, but part of it wasn’t built` : live,\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}`\n : drafts > 0\n ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.`\n : at\n ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n\n/**\n * The one reminder a plan that waits for its confirmation gets (AGL-4076),\n * a day after it was ready. The same kind of notice as the first one\n * (`content.aiJobNeedsYou`), so the same switch in the person's notification\n * settings turns both off, and it opens the same place: AI jobs on the job, or\n * a site start's build page. It says it is the only one, because it is.\n */\nexport function aiJobPlanReminderNotice(job: AiJobNoticeSource, site: AiJobNoticeSite | null = null): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, 'needs-review')\n const on = site ? ` for ${site.name}` : ''\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun}${on} still needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs. This is the only reminder.`,\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: noun === 'AI job' ? `Your plan${on} is still waiting for you` : `Your ${noun} plan${on} is still waiting for you`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs. This is the only reminder.',\n link,\n ...scope,\n }\n}\n"],"names":["besignerOpenDraftHref","aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","besigner","id","inDraft","aiJobNotice","site","scope","orgId","link","input","message","type","level","title","body","review","reason","error","page","Boolean","join","items","unbuilt","some","row","partial","published","sitePublish","drafts","stayed","live","name","at","url","pages","aiJobPlanReminderNotice","on"],"mappings":"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"}
|
|
@@ -66,10 +66,10 @@ export declare const ASSIST_EDIT_SELECTED_TEXT_CHARS = 400;
|
|
|
66
66
|
export declare const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80;
|
|
67
67
|
/**
|
|
68
68
|
* Elements a whole-document outline describes at most: one the server reads
|
|
69
|
-
* from the stored document for an edit job,
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* from the stored document for an edit job, or the chat in the Besigner sends
|
|
70
|
+
* of the open canvas (AGL-4116). Far past a page an AI build or a starter makes
|
|
71
|
+
* (the starter home has 182), so in practice the model sees every element it
|
|
72
|
+
* could be asked about.
|
|
73
73
|
*/
|
|
74
74
|
export declare const ASSIST_EDIT_WHOLE_MAX_NODES = 400;
|
|
75
75
|
/**
|
|
@@ -143,9 +143,10 @@ export interface AssistEditCanvasContext {
|
|
|
143
143
|
total?: number;
|
|
144
144
|
nodes: AssistEditCanvasNode[];
|
|
145
145
|
/**
|
|
146
|
-
* The document
|
|
147
|
-
*
|
|
148
|
-
*
|
|
146
|
+
* The whole document, every element in page order up to
|
|
147
|
+
* {@link ASSIST_EDIT_WHOLE_MAX_NODES}, each with its settings and styles:
|
|
148
|
+
* read from the store for a job, or from the open canvas for the chat in the
|
|
149
|
+
* Besigner (AGL-4116).
|
|
149
150
|
*/
|
|
150
151
|
whole?: true;
|
|
151
152
|
}
|
|
@@ -74,10 +74,10 @@ const BESIGNER_DOCUMENT_ROUTE = /^\/[^/]+\/hosts\/[^/]+\/(screens|components|lay
|
|
|
74
74
|
/** Characters of a string prop every other element carries. */ export const ASSIST_EDIT_OUTLINE_TEXT_CHARS = 80;
|
|
75
75
|
/**
|
|
76
76
|
* Elements a whole-document outline describes at most: one the server reads
|
|
77
|
-
* from the stored document for an edit job,
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* from the stored document for an edit job, or the chat in the Besigner sends
|
|
78
|
+
* of the open canvas (AGL-4116). Far past a page an AI build or a starter makes
|
|
79
|
+
* (the starter home has 182), so in practice the model sees every element it
|
|
80
|
+
* could be asked about.
|
|
81
81
|
*/ export const ASSIST_EDIT_WHOLE_MAX_NODES = 400;
|
|
82
82
|
/* ------------------------------------------------------------------ *
|
|
83
83
|
* The interactions Assist may add (AGL-3603)
|
|
@@ -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 * 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":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD;;;;;;;;;;;;;;;;CAgBC,GAED,8CAA8C,GAC9C,OAAO,MAAMA,uBAAuB;IAClC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV,4EAA4E,GAC5E,OAAO,MAAMC,wBAAwB,cAAa;AAElD,6EAA6E,GAC7E,OAAO,MAAMC,wBAAwB,sBAAqB;AAK1D,OAAO,MAAMC,6BAAgE;IAC3E;IACA;IACA;CACD,CAAA;AAED,MAAMC,mBAAqE;IACzEC,SAAS;IACTC,YAAY;IACZC,SAAS;AACX;AAEA,MAAMC,0BACJ;AASF;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAa;IAChD,MAAMC,QAAQH,wBAAwBI,IAAI,CAACC,OAAOH,gBAAAA,QAAS;IAC3D,IAAI,CAACC,OAAO,OAAO;IACnB,OAAO;QACLG,MAAMV,gBAAgB,CAACO,KAAK,CAAC,EAAE,CAAC;QAChCI,YAAYJ,KAAK,CAAC,EAAE;QACpBK,WAAWL,KAAK,CAAC,EAAE;IACrB;AACF;AAEA;;sEAEsE,GAEtE,yFAAyF,GACzF,OAAO,MAAMM,gCAAgC,GAAE;AAC/C,yCAAyC,GACzC,OAAO,MAAMC,gCAAgC,GAAE;AAC/C,6EAA6E,GAC7E,OAAO,MAAMC,kCAAkC,IAAG;AAClD,6DAA6D,GAC7D,OAAO,MAAMC,iCAAiC,GAAE;AAChD;;;;;;CAMC,GACD,OAAO,MAAMC,8BAA8B,IAAG;AAkD9C;;sEAEsE,GAEtE;;;;CAIC,GACD,OAAO,MAAMC,iCAAiC;IAC5C;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC;IAC3C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAqLV,6EAA6E,GAC7E,OAAO,MAAMC,kCAAkC,GAAE;AAajD,kCAAkC,GAClC,OAAO,SAASC,uBAAuBC,GAA4B;IACjE,MAAMC,OAAuB;QAC3BC,OAAO;QACPC,SAAS;QACTC,cAAc;QACdC,eAAe;QACfC,OAAO;QACPC,SAAS;QACTC,WAAW;QACXC,gBAAgB;QAChBC,iBAAiB;QACjBC,mBAAmB;IACrB;IACA,KAAK,MAAMC,MAAMZ,IAAK;QACpB,OAAQY,GAAGA,EAAE;YACX,KAAK;gBACHX,KAAKC,KAAK,IAAIW,OAAOC,IAAI,CAACF,GAAGG,KAAK,EAAEC,MAAM;gBAC1C;YACF,KAAK;gBACHf,KAAKE,OAAO,IAAI;gBAChB;YACF,KAAK;gBACHF,KAAKG,YAAY,IAAI;gBACrB;YACF,KAAK;gBACHH,KAAKI,aAAa,IAAI;gBACtB;YACF,KAAK;gBACHJ,KAAKK,KAAK,IAAI;gBACd;YACF,KAAK;gBACHL,KAAKM,OAAO,IAAI;gBAChB;YACF,KAAK;gBACHN,KAAKO,SAAS,IAAIK,OAAOC,IAAI,CAACF,GAAGK,MAAM,EAAED,MAAM;gBAC/C;YACF,KAAK;gBACHf,KAAKS,eAAe,IAAI;gBACxBT,KAAKQ,cAAc,IAAIG,GAAGM,KAAK,CAACF,MAAM;gBACtC;YACF,KAAK;gBACHf,KAAKU,iBAAiB,IAAI;gBAC1B;QACJ;IACF;IACA,OAAOV;AACT;AAEA,MAAMkB,SAAS,CAACC,OAAeC,KAAaC,OAC1C,GAAGF,MAAM,CAAC,EAAEA,UAAU,IAAIC,MAAMC,MAAM;AAExC,+DAA+D,GAC/D,OAAO,SAASC,uBAAuBtB,IAAoB;IACzD,MAAMuB,QAAkB,EAAE;IAC1B,IAAIvB,KAAKC,KAAK,EAAEsB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKC,KAAK,EAAE,WAAW,YAAY,MAAM,CAAC;IAC/E,IAAID,KAAKE,OAAO,EAAEqB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKE,OAAO,EAAE,WAAW,YAAY,QAAQ,CAAC;IACrF,IAAIF,KAAKG,YAAY,EAAE;QACrBoB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKG,YAAY,EAAE,sBAAsB,sBAAsB,QAAQ,CAAC;IAC/F;IACA,IAAIH,KAAKI,aAAa,EAAE;QACtBmB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKI,aAAa,EAAE,WAAW,YAAY,SAAS,CAAC;IAC5E;IACA,IAAIJ,KAAKK,KAAK,EAAEkB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKK,KAAK,EAAE,WAAW,YAAY,MAAM,CAAC;IAC/E,IAAIL,KAAKM,OAAO,EAAEiB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKM,OAAO,EAAE,SAAS,UAAU,QAAQ,CAAC;IACjF,IAAIN,KAAKO,SAAS,EAAE;QAClBgB,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKO,SAAS,EAAE,gBAAgB,iBAAiB,UAAU,CAAC;IACnF;IACA,IAAIP,KAAKU,iBAAiB,EAAE;QAC1Ba,MAAMC,IAAI,CAAC,GAAGN,OAAOlB,KAAKU,iBAAiB,EAAE,eAAe,gBAAgB,MAAM,CAAC;IACrF;IACA,IAAIV,KAAKS,eAAe,EAAE;QACxBc,MAAMC,IAAI,CACR,CAAC,mCAAmC,EAAEN,OAAOlB,KAAKQ,cAAc,EAAE,YAAY,eAAe;IAEjG;IACA,OAAOe;AACT;AAEA,gEAAgE,GAChE,MAAME,iBAA6D;IACjEC,aAAa;IACbC,UAAU;IACVC,eAAe;IACfC,MAAM;IACNC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;IACRC,iBAAiB;IACjBC,gBAAgB;AAClB;AAEA,+FAA+F,GAC/F,OAAO,SAASC,mBAAmBpC,GAA4B;IAC7D,MAAMqC,SAAiC,CAAC;IACxC,KAAK,MAAMzB,MAAMZ,IAAK;YAEMqC;QAD1B,MAAMC,OAAOZ,cAAc,CAACd,GAAGA,EAAE,CAAC;QAClC,IAAI0B,MAAMD,MAAM,CAACC,KAAK,GAAG,EAACD,eAAAA,MAAM,CAACC,KAAK,YAAZD,eAAgB,KAAK;IACjD;IACA,OAAOA;AACT;AAEA,kFAAkF,GAClF,OAAO,MAAME,6BAAgD1B,OAAO2B,MAAM,CAACd,gBAAe;AAE1F,MAAMe,WAAW,CAACC,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AAEhE;;;;CAIC,GACD,OAAO,SAASG,qBAAqBH,KAAc;IACjD,IAAI,CAACD,SAASC,UAAUA,KAAK,CAAC,KAAK,KAAKnE,uBAAuB,OAAO;IACtE,MAAMuE,SAASJ,KAAK,CAAC,SAAS;IAC9B,IAAI,CAACD,SAASK,SAAS,OAAO;IAC9B,IAAI,CAACrE,2BAA2BsE,QAAQ,CAACD,MAAM,CAAC,OAAO,GAA6B,OAAO;IAC3F,IAAI,OAAOA,MAAM,CAAC,aAAa,KAAK,YAAY,CAACA,MAAM,CAAC,aAAa,EAAE,OAAO;IAC9E,MAAM9C,MAAM0C,KAAK,CAAC,MAAM;IACxB,IAAI,CAACC,MAAMC,OAAO,CAAC5C,QAAQ,CAACA,IAAIgB,MAAM,EAAE,OAAO;IAC/C,OAAOhB,IAAIgD,KAAK,CACd,CAACpC,KAAO6B,SAAS7B,OAAOtC,qBAAqByE,QAAQ,CAACnC,EAAE,CAAC,KAAK;AAElE"}
|
|
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, or the chat in the Besigner sends\n * of the open canvas (AGL-4116). Far past a page an AI build or a starter makes\n * (the starter home has 182), so in practice the model sees every element it\n * 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 whole document, every element in page order up to\n * {@link ASSIST_EDIT_WHOLE_MAX_NODES}, each with its settings and styles:\n * read from the store for a job, or from the open canvas for the chat in the\n * Besigner (AGL-4116).\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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"}
|
|
@@ -84,3 +84,17 @@ export declare function assistPageChangeDraftVersion(changes: ReadonlyArray<{
|
|
|
84
84
|
pageChange?: AssistPageChangeProposal | null;
|
|
85
85
|
pageChangeJob?: AiJobSummary | null;
|
|
86
86
|
}>, pageId: string): string | null;
|
|
87
|
+
/** What a page change came to, as the turn that asked for it holds it. */
|
|
88
|
+
export interface AssistPageChangeTurn {
|
|
89
|
+
pageChange?: AssistPageChangeProposal | null;
|
|
90
|
+
pageChangeJob?: AiJobSummary | null;
|
|
91
|
+
pageChangeNotice?: string | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What became of a turn's page change, in the words the next question's
|
|
95
|
+
* history carries it (AGL-4116), or null when the turn asked for none. The
|
|
96
|
+
* chat sees a thread's text and nothing else, so without this it read its own
|
|
97
|
+
* "I'll make this change" with no outcome and told the person nothing in the
|
|
98
|
+
* chat had changed their page.
|
|
99
|
+
*/
|
|
100
|
+
export declare function assistPageChangeHistoryNote(turn: AssistPageChangeTurn): string | null;
|
|
@@ -82,5 +82,26 @@ function isRecord(value) {
|
|
|
82
82
|
}
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* What became of a turn's page change, in the words the next question's
|
|
87
|
+
* history carries it (AGL-4116), or null when the turn asked for none. The
|
|
88
|
+
* chat sees a thread's text and nothing else, so without this it read its own
|
|
89
|
+
* "I'll make this change" with no outcome and told the person nothing in the
|
|
90
|
+
* chat had changed their page.
|
|
91
|
+
*/ export function assistPageChangeHistoryNote(turn) {
|
|
92
|
+
var _turn_pageChangeJob, _output_note;
|
|
93
|
+
const proposal = turn.pageChange;
|
|
94
|
+
if (!proposal) return null;
|
|
95
|
+
const change = `Page change on ${proposal.pageName}`;
|
|
96
|
+
if (turn.pageChangeNotice) return `(${change} not started: ${turn.pageChangeNotice})`;
|
|
97
|
+
const job = (_turn_pageChangeJob = turn.pageChangeJob) != null ? _turn_pageChangeJob : null;
|
|
98
|
+
const output = assistPageChangeOutput(job);
|
|
99
|
+
if (output) return `(${change}: ${(_output_note = output.note) != null ? _output_note : 'saved, not published.'})`;
|
|
100
|
+
if ((job == null ? void 0 : job.status) === 'failed' || (job == null ? void 0 : job.status) === 'canceled') {
|
|
101
|
+
var _job_error;
|
|
102
|
+
return `(${change} not made: ${(_job_error = job.error) != null ? _job_error : job.status})`;
|
|
103
|
+
}
|
|
104
|
+
return `(${change} being made.)`;
|
|
105
|
+
}
|
|
85
106
|
|
|
86
107
|
//# sourceMappingURL=assist-page-change.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/assist-page-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A change to a page the site already has, asked in the Assist chat\n * (AGL-4116). No plan and no new page: the panel starts one `edit` job on\n * that page, which saves the change into the page's draft — on every plan,\n * the draft the Besigner offers with Open draft — and the card links the\n * page in the Besigner, where the person checks it and publishes it. Only\n * when they ask for a new version, and the plan keeps version history, is\n * the change saved as a new unpublished version instead.\n *\n * Asked through a build, a change to the home page came back as a second\n * page at `/` the plan generated from scratch, and every follow-up made\n * another plan. This is the short way, for the request that only changes\n * what is there.\n *\n * Shared by the chat door that resolves it and the panel that starts it.\n * Pure: no request, no React.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/** The strict tool the chat model asks for a change to a page through. */\nexport const ASSIST_PAGE_CHANGE_TOOL_NAME = 'change_page'\n\n/** The proposal's fixed id, as `assistant_proposal_shown` records it. */\nexport const ASSIST_PAGE_CHANGE_ACTION_ID = 'page-change'\n\n/** The longest one-line summary the card shows. */\nexport const ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS = 200\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,100}$/\n\nexport interface AssistPageChangeProposal {\n id: typeof ASSIST_PAGE_CHANGE_ACTION_ID\n /** The site the request was asked on; the job is created for it. */\n hostId: string\n /** The page to change: one the site has, by its id. */\n pageId: string\n /** The page's name, as the site's list of pages gives it. */\n pageName: string\n /** What to change on it, in the person's words. */\n brief: string\n /** One line saying what will change. */\n summary: string\n /** They asked for a new version of the page; otherwise the change goes into its draft. */\n newVersion?: boolean\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Shape-checks a page change before it can start a job. */\nexport function isAssistPageChangeProposal(value: unknown): value is AssistPageChangeProposal {\n if (!isRecord(value) || value['id'] !== ASSIST_PAGE_CHANGE_ACTION_ID) return false\n return (\n typeof value['hostId'] === 'string' &&\n DOCUMENT_ID.test(value['hostId']) &&\n typeof value['pageId'] === 'string' &&\n DOCUMENT_ID.test(value['pageId']) &&\n typeof value['pageName'] === 'string' &&\n typeof value['brief'] === 'string' &&\n value['brief'].trim().length > 0 &&\n typeof value['summary'] === 'string' &&\n (value['newVersion'] === undefined || typeof value['newVersion'] === 'boolean')\n )\n}\n\n/**\n * The body of `POST /api/ai/jobs` a page change starts: an `edit` of the\n * page. `versionId` is the version an earlier change in the same\n * conversation wrote to, so a follow-up changes that version's draft rather\n * than starting again from the published page.\n */\nexport function assistPageChangeJobRequest(\n orgId: string,\n proposal: AssistPageChangeProposal,\n versionId: string | null,\n): {\n orgId: string\n hostId: string\n kind: 'edit'\n brief: string\n inputs: { target: string; targetKind: 'screen'; versionId?: string; newVersion?: true }\n} {\n return {\n orgId,\n hostId: proposal.hostId,\n kind: 'edit',\n brief: proposal.brief,\n inputs: {\n target: proposal.pageId,\n targetKind: 'screen',\n ...(versionId ? { versionId } : {}),\n ...(proposal.newVersion ? { newVersion: true as const } : {}),\n },\n }\n}\n\n/** The version a finished change wrote to the page, or `null` before there is one. */\nexport function assistPageChangeOutput(job: Pick<AiJobSummary, 'status' | 'outputs'> | null): AiJobOutput | null {\n if (!job || job.status !== 'done') return null\n return job.outputs.find((output) => output.resource === 'screen' && Boolean(output.versionId)) ?? null\n}\n\n/**\n * The version a new change to `pageId` continues (AGL-4116): the one the\n * conversation's latest finished change to that page wrote to — its draft\n * holds that change — or `null` to start from the page as it is.\n */\nexport function assistPageChangeDraftVersion(\n changes: ReadonlyArray<{ pageChange?: AssistPageChangeProposal | null; pageChangeJob?: AiJobSummary | null }>,\n pageId: string,\n): string | null {\n for (let index = changes.length - 1; index >= 0; index -= 1) {\n const change = changes[index]\n if (change.pageChange?.pageId !== pageId) continue\n const output = assistPageChangeOutput(change.pageChangeJob ?? null)\n if (output?.versionId) return output.versionId\n }\n return null\n}\n"],"names":["ASSIST_PAGE_CHANGE_TOOL_NAME","ASSIST_PAGE_CHANGE_ACTION_ID","ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS","DOCUMENT_ID","isRecord","value","Array","isArray","isAssistPageChangeProposal","test","trim","length","undefined","assistPageChangeJobRequest","orgId","proposal","versionId","hostId","kind","brief","inputs","target","pageId","targetKind","newVersion","assistPageChangeOutput","job","status","outputs","find","output","resource","Boolean","assistPageChangeDraftVersion","changes","index","change","pageChange","pageChangeJob"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC;AAID,wEAAwE,GACxE,OAAO,MAAMA,+BAA+B,cAAa;AAEzD,uEAAuE,GACvE,OAAO,MAAMC,+BAA+B,cAAa;AAEzD,iDAAiD,GACjD,OAAO,MAAMC,uCAAuC,IAAG;AAEvD,MAAMC,cAAc;AAkBpB,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AACvE;AAEA,0DAA0D,GAC1D,OAAO,SAASG,2BAA2BH,KAAc;IACvD,IAAI,CAACD,SAASC,UAAUA,KAAK,CAAC,KAAK,KAAKJ,8BAA8B,OAAO;IAC7E,OACE,OAAOI,KAAK,CAAC,SAAS,KAAK,YAC3BF,YAAYM,IAAI,CAACJ,KAAK,CAAC,SAAS,KAChC,OAAOA,KAAK,CAAC,SAAS,KAAK,YAC3BF,YAAYM,IAAI,CAACJ,KAAK,CAAC,SAAS,KAChC,OAAOA,KAAK,CAAC,WAAW,KAAK,YAC7B,OAAOA,KAAK,CAAC,QAAQ,KAAK,YAC1BA,KAAK,CAAC,QAAQ,CAACK,IAAI,GAAGC,MAAM,GAAG,KAC/B,OAAON,KAAK,CAAC,UAAU,KAAK,YAC3BA,CAAAA,KAAK,CAAC,aAAa,KAAKO,aAAa,OAAOP,KAAK,CAAC,aAAa,KAAK,SAAQ;AAEjF;AAEA;;;;;CAKC,GACD,OAAO,SAASQ,2BACdC,KAAa,EACbC,QAAkC,EAClCC,SAAwB;IAQxB,OAAO;QACLF;QACAG,QAAQF,SAASE,MAAM;QACvBC,MAAM;QACNC,OAAOJ,SAASI,KAAK;QACrBC,QAAQ;YACNC,QAAQN,SAASO,MAAM;YACvBC,YAAY;WACRP,YAAY;YAAEA;QAAU,IAAI,CAAC,GAC7BD,SAASS,UAAU,GAAG;YAAEA,YAAY;QAAc,IAAI,CAAC;IAE/D;AACF;AAEA,oFAAoF,GACpF,OAAO,SAASC,uBAAuBC,GAAoD;QAElFA;IADP,IAAI,CAACA,OAAOA,IAAIC,MAAM,KAAK,QAAQ,OAAO;IAC1C,QAAOD,oBAAAA,IAAIE,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,QAAQ,KAAK,YAAYC,QAAQF,OAAOd,SAAS,cAArFU,oBAA2F;AACpG;AAEA;;;;CAIC,GACD,OAAO,SAASO,6BACdC,OAA6G,EAC7GZ,MAAc;IAEd,IAAK,IAAIa,QAAQD,QAAQvB,MAAM,GAAG,GAAGwB,SAAS,GAAGA,SAAS,EAAG;YAGrBC;YADlCA;QADJ,MAAMA,SAASF,OAAO,CAACC,MAAM;QAC7B,IAAIC,EAAAA,qBAAAA,OAAOC,UAAU,qBAAjBD,mBAAmBd,MAAM,MAAKA,QAAQ;QAC1C,MAAMQ,SAASL,wBAAuBW,wBAAAA,OAAOE,aAAa,YAApBF,wBAAwB;QAC9D,IAAIN,0BAAAA,OAAQd,SAAS,EAAE,OAAOc,OAAOd,SAAS;IAChD;IACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/assist-page-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A change to a page the site already has, asked in the Assist chat\n * (AGL-4116). No plan and no new page: the panel starts one `edit` job on\n * that page, which saves the change into the page's draft — on every plan,\n * the draft the Besigner offers with Open draft — and the card links the\n * page in the Besigner, where the person checks it and publishes it. Only\n * when they ask for a new version, and the plan keeps version history, is\n * the change saved as a new unpublished version instead.\n *\n * Asked through a build, a change to the home page came back as a second\n * page at `/` the plan generated from scratch, and every follow-up made\n * another plan. This is the short way, for the request that only changes\n * what is there.\n *\n * Shared by the chat door that resolves it and the panel that starts it.\n * Pure: no request, no React.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/** The strict tool the chat model asks for a change to a page through. */\nexport const ASSIST_PAGE_CHANGE_TOOL_NAME = 'change_page'\n\n/** The proposal's fixed id, as `assistant_proposal_shown` records it. */\nexport const ASSIST_PAGE_CHANGE_ACTION_ID = 'page-change'\n\n/** The longest one-line summary the card shows. */\nexport const ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS = 200\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,100}$/\n\nexport interface AssistPageChangeProposal {\n id: typeof ASSIST_PAGE_CHANGE_ACTION_ID\n /** The site the request was asked on; the job is created for it. */\n hostId: string\n /** The page to change: one the site has, by its id. */\n pageId: string\n /** The page's name, as the site's list of pages gives it. */\n pageName: string\n /** What to change on it, in the person's words. */\n brief: string\n /** One line saying what will change. */\n summary: string\n /** They asked for a new version of the page; otherwise the change goes into its draft. */\n newVersion?: boolean\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Shape-checks a page change before it can start a job. */\nexport function isAssistPageChangeProposal(value: unknown): value is AssistPageChangeProposal {\n if (!isRecord(value) || value['id'] !== ASSIST_PAGE_CHANGE_ACTION_ID) return false\n return (\n typeof value['hostId'] === 'string' &&\n DOCUMENT_ID.test(value['hostId']) &&\n typeof value['pageId'] === 'string' &&\n DOCUMENT_ID.test(value['pageId']) &&\n typeof value['pageName'] === 'string' &&\n typeof value['brief'] === 'string' &&\n value['brief'].trim().length > 0 &&\n typeof value['summary'] === 'string' &&\n (value['newVersion'] === undefined || typeof value['newVersion'] === 'boolean')\n )\n}\n\n/**\n * The body of `POST /api/ai/jobs` a page change starts: an `edit` of the\n * page. `versionId` is the version an earlier change in the same\n * conversation wrote to, so a follow-up changes that version's draft rather\n * than starting again from the published page.\n */\nexport function assistPageChangeJobRequest(\n orgId: string,\n proposal: AssistPageChangeProposal,\n versionId: string | null,\n): {\n orgId: string\n hostId: string\n kind: 'edit'\n brief: string\n inputs: { target: string; targetKind: 'screen'; versionId?: string; newVersion?: true }\n} {\n return {\n orgId,\n hostId: proposal.hostId,\n kind: 'edit',\n brief: proposal.brief,\n inputs: {\n target: proposal.pageId,\n targetKind: 'screen',\n ...(versionId ? { versionId } : {}),\n ...(proposal.newVersion ? { newVersion: true as const } : {}),\n },\n }\n}\n\n/** The version a finished change wrote to the page, or `null` before there is one. */\nexport function assistPageChangeOutput(job: Pick<AiJobSummary, 'status' | 'outputs'> | null): AiJobOutput | null {\n if (!job || job.status !== 'done') return null\n return job.outputs.find((output) => output.resource === 'screen' && Boolean(output.versionId)) ?? null\n}\n\n/**\n * The version a new change to `pageId` continues (AGL-4116): the one the\n * conversation's latest finished change to that page wrote to — its draft\n * holds that change — or `null` to start from the page as it is.\n */\nexport function assistPageChangeDraftVersion(\n changes: ReadonlyArray<{ pageChange?: AssistPageChangeProposal | null; pageChangeJob?: AiJobSummary | null }>,\n pageId: string,\n): string | null {\n for (let index = changes.length - 1; index >= 0; index -= 1) {\n const change = changes[index]\n if (change.pageChange?.pageId !== pageId) continue\n const output = assistPageChangeOutput(change.pageChangeJob ?? null)\n if (output?.versionId) return output.versionId\n }\n return null\n}\n\n/** What a page change came to, as the turn that asked for it holds it. */\nexport interface AssistPageChangeTurn {\n pageChange?: AssistPageChangeProposal | null\n pageChangeJob?: AiJobSummary | null\n pageChangeNotice?: string | null\n}\n\n/**\n * What became of a turn's page change, in the words the next question's\n * history carries it (AGL-4116), or null when the turn asked for none. The\n * chat sees a thread's text and nothing else, so without this it read its own\n * \"I'll make this change\" with no outcome and told the person nothing in the\n * chat had changed their page.\n */\nexport function assistPageChangeHistoryNote(turn: AssistPageChangeTurn): string | null {\n const proposal = turn.pageChange\n if (!proposal) return null\n const change = `Page change on ${proposal.pageName}`\n if (turn.pageChangeNotice) return `(${change} not started: ${turn.pageChangeNotice})`\n const job = turn.pageChangeJob ?? null\n const output = assistPageChangeOutput(job)\n if (output) return `(${change}: ${output.note ?? 'saved, not published.'})`\n if (job?.status === 'failed' || job?.status === 'canceled') {\n return `(${change} not made: ${job.error ?? job.status})`\n }\n return `(${change} being made.)`\n}\n"],"names":["ASSIST_PAGE_CHANGE_TOOL_NAME","ASSIST_PAGE_CHANGE_ACTION_ID","ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS","DOCUMENT_ID","isRecord","value","Array","isArray","isAssistPageChangeProposal","test","trim","length","undefined","assistPageChangeJobRequest","orgId","proposal","versionId","hostId","kind","brief","inputs","target","pageId","targetKind","newVersion","assistPageChangeOutput","job","status","outputs","find","output","resource","Boolean","assistPageChangeDraftVersion","changes","index","change","pageChange","pageChangeJob","assistPageChangeHistoryNote","turn","pageName","pageChangeNotice","note","error"],"mappings":"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"}
|
|
@@ -524,7 +524,11 @@ ${AI_ACCEPTABLE_USE_BLOCK}`;
|
|
|
524
524
|
const hostId = sanitiseId(body.context.hostId);
|
|
525
525
|
if (!document || !hostId) return null;
|
|
526
526
|
if (!checkEntitlement(org, 'aiGenerative')) return null;
|
|
527
|
-
|
|
527
|
+
// The chat in the Besigner sends the whole page when it fits (AGL-4116),
|
|
528
|
+
// held to the same limits as a job's read of it.
|
|
529
|
+
const context = parseAssistEditContext(body.canvas, {
|
|
530
|
+
whole: body.canvas.whole === true
|
|
531
|
+
});
|
|
528
532
|
if (!context) return null;
|
|
529
533
|
if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', body.orgId)) {
|
|
530
534
|
return null;
|