@aglyn/plugins-ai 1.0.0-beta.296 → 1.1.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.
Files changed (70) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +2 -1
  3. package/src/lib/components/ai-job-links.js +8 -2
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  6. package/src/lib/components/ai-site-start-card.component.js +40 -13
  7. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  8. package/src/lib/components/ai-site-start-steps.component.d.ts +17 -1
  9. package/src/lib/components/ai-site-start-steps.component.js +30 -7
  10. package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
  11. package/src/lib/components/assist-edit-canvas.d.ts +12 -6
  12. package/src/lib/components/assist-edit-canvas.js +86 -36
  13. package/src/lib/components/assist-edit-canvas.js.map +1 -1
  14. package/src/lib/components/assist-edit-card.component.js +9 -8
  15. package/src/lib/components/assist-edit-card.component.js.map +1 -1
  16. package/src/lib/components/assist-page-change-card.component.d.ts +1 -1
  17. package/src/lib/components/assist-page-change-card.component.js +2 -2
  18. package/src/lib/components/assist-page-change-card.component.js.map +1 -1
  19. package/src/lib/components/assist-panel.component.js +16 -9
  20. package/src/lib/components/assist-panel.component.js.map +1 -1
  21. package/src/lib/jobs/ai-job-drafts.d.ts +3 -0
  22. package/src/lib/jobs/ai-job-drafts.js +5 -0
  23. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  24. package/src/lib/jobs/ai-job-edit-step.d.ts +20 -0
  25. package/src/lib/jobs/ai-job-edit-step.js +39 -3
  26. package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-layout-language.d.ts +10 -0
  28. package/src/lib/jobs/ai-job-layout-language.js +38 -2
  29. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  30. package/src/lib/jobs/ai-job-layout-step.js +3 -3
  31. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  32. package/src/lib/jobs/ai-site-import-compile.js +15 -7
  33. package/src/lib/jobs/ai-site-import-compile.js.map +1 -1
  34. package/src/lib/layout-language/ai-layout-compiler.js +39 -1
  35. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  36. package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
  37. package/src/lib/layout-language/ai-layout-frame.js +85 -30
  38. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  39. package/src/lib/layout-language/ai-layout-language.d.ts +5 -0
  40. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  41. package/src/lib/model/ai-edit-job.js +1 -1
  42. package/src/lib/model/ai-edit-job.js.map +1 -1
  43. package/src/lib/model/ai-job-notice.js +3 -1
  44. package/src/lib/model/ai-job-notice.js.map +1 -1
  45. package/src/lib/model/ai-site-import-fidelity.d.ts +175 -0
  46. package/src/lib/model/ai-site-import-fidelity.js +941 -0
  47. package/src/lib/model/ai-site-import-fidelity.js.map +1 -0
  48. package/src/lib/model/ai-site-start.d.ts +27 -0
  49. package/src/lib/model/ai-site-start.js +41 -0
  50. package/src/lib/model/ai-site-start.js.map +1 -1
  51. package/src/lib/model/assist-edit.d.ts +8 -7
  52. package/src/lib/model/assist-edit.js +4 -4
  53. package/src/lib/model/assist-edit.js.map +1 -1
  54. package/src/lib/model/assist-page-change.d.ts +34 -0
  55. package/src/lib/model/assist-page-change.js +43 -0
  56. package/src/lib/model/assist-page-change.js.map +1 -1
  57. package/src/lib/runtime/ai-node-tree.js +18 -2
  58. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  59. package/src/lib/server/assist-chat.d.ts +5 -0
  60. package/src/lib/server/assist-chat.js +16 -3
  61. package/src/lib/server/assist-chat.js.map +1 -1
  62. package/src/lib/server/assist-edit.d.ts +12 -7
  63. package/src/lib/server/assist-edit.js +43 -22
  64. package/src/lib/server/assist-edit.js.map +1 -1
  65. package/src/lib/server/assist-page-change.d.ts +12 -0
  66. package/src/lib/server/assist-page-change.js +24 -2
  67. package/src/lib/server/assist-page-change.js.map +1 -1
  68. package/src/lib/site-import/fixtures/site-import-fidelity-replay.d.ts +62 -0
  69. package/src/lib/site-import/fixtures/site-import-fidelity-replay.js +374 -0
  70. package/src/lib/site-import/fixtures/site-import-fidelity-replay.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-language.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 { AiTool } from '../providers/contract'\nimport { AI_ICON_WORDS } from '../runtime/ai-icon-library'\n\n/**\n * The compact layout language (AGL-3660): what a model designs a page, and a\n * site's header and footer, in — and the one place its shape is declared.\n *\n * ── Why a language, not a tree ───────────────────────────────────────────\n *\n * A page used to be asked for as a raw element tree, packed as escaped JSON\n * text inside a tool call and held to the doctrine's building rules: Grid\n * items sized as strings, every child attached, one h1, a destination on\n * every button, headings under their ceiling. A real model kept about half\n * of a page's sections on the first try, and a Free section was cut off at\n * its token ceiling. The founder chose a language over a library of section\n * templates (10/7): every page is designed for its own request, so no two\n * sites come out alike, and the rules hold because CODE writes the tree.\n *\n * So the model says WHAT each section is and how it is arranged, in a few\n * words of structure, and writes every word a visitor reads; the compiler\n * (`ai-layout-compiler.ts`) turns that into the element tree with the\n * palette's elements and the theme's tokens, by construction.\n *\n * ── The shape ────────────────────────────────────────────────────────────\n *\n * A page is its plan's sections, in order. A SECTION is\n * - `band` — its background: `plain` (the page), `soft` (a paper band),\n * `brand` (the brand color) or `dark` (always dark);\n * - `cols` — a row of columns by relative width, as `[7, 5]` or `[1, 1, 1]`,\n * up to four; absent, one column;\n * - `align` — `start` or `center`;\n * - `blocks` — what it shows, top to bottom.\n *\n * A BLOCK is one thing a visitor reads or uses. Its `kind` is one of\n * `AI_LAYOUT_BLOCK_KINDS`; `col` places it in a column of the row (0 first),\n * and a block with no `col` spans the section — above the row when it comes\n * before the first placed block, below it when it comes after. `text` is its\n * words, `to` where it goes, `style` its emphasis and `icon` a word from the\n * icon library. A group — `list`, `cards`, `steps`, `stats`, `quotes`,\n * `faq` — carries its `items`, each a `title`, a `text`, and optionally its\n * own `to` and `icon`.\n *\n * Where a block goes (`to`): `page:<id>` for a page of this site (its label\n * works too), `#<n>` for section n of this page, `form` for the site's form,\n * `phone` or `email` for the phone number or email the brief gives (a link\n * that calls or writes, AGL-3979), a form's or a component's id for a `form`\n * or `component` block, or an `https:` address the brief gave.\n *\n * A site's header and footer are two sections in the same language\n * (`AI_LAYOUT_FRAME_TOOL`): the header's brand and navigation are the\n * site's own and written by the compiler, so the model gives the header its\n * band, its alignment and an optional call-to-action button; the footer is\n * a section like any other.\n *\n * ── Cheap by shape ───────────────────────────────────────────────────────\n *\n * The schema is a structured object in the tool input (never JSON text in a\n * string), small enough for a strict tool (`tool-schema-limits.spec.ts`), and\n * the language's instructions ride in the cached prefix. A section is a few\n * dozen words of structure beside its copy, so a whole page is one answer.\n */\n\n/** Every kind of block, in the order the schema lists them. */\nexport const AI_LAYOUT_BLOCK_KINDS = [\n 'eyebrow',\n 'heading',\n 'lede',\n 'text',\n 'note',\n 'list',\n 'button',\n 'image',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n 'form',\n 'component',\n] as const\n\nexport type AiLayoutBlockKind = (typeof AI_LAYOUT_BLOCK_KINDS)[number]\n\n/** The kinds that carry `items`. */\nexport const AI_LAYOUT_GROUP_KINDS: ReadonlySet<string> = new Set<AiLayoutBlockKind>([\n 'list',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n])\n\nexport const AI_LAYOUT_BANDS = ['plain', 'soft', 'brand', 'dark'] as const\nexport type AiLayoutBand = (typeof AI_LAYOUT_BANDS)[number]\n\nexport const AI_LAYOUT_ALIGNS = ['start', 'center'] as const\nexport type AiLayoutAlign = (typeof AI_LAYOUT_ALIGNS)[number]\n\nexport const AI_LAYOUT_STYLES = [\n 'primary',\n 'secondary',\n 'quiet',\n 'large',\n] as const\nexport type AiLayoutStyle = (typeof AI_LAYOUT_STYLES)[number]\n\n/** The most columns a row holds; a fifth is folded into the fourth. */\nexport const AI_LAYOUT_MAX_COLS = 4\n\n/**\n * The most items a group shows: one under the eight copies rule 8 reads as a\n * list typed out by hand, which belongs in a dataset.\n */\nexport const AI_LAYOUT_MAX_ITEMS = 7\n\n/** The most quotes a group shows: each is a gap the owner fills with a real one. */\nexport const AI_LAYOUT_MAX_QUOTES = 3\n\nexport interface AiLayoutItem {\n title: string\n text: string\n to?: string\n icon?: string\n // ── A page brought in (AGL-4107): what the source's item shows beside its\n // title and text, every word as written. No model writes these: no tool\n // schema names them, and the compiler draws them only on a verbatim page.\n /** Its own icon, the source's drawing as a path on the 24-unit square. */\n iconPath?: string\n /** A figure that leads it: a step's number, a plan's price. */\n figure?: string\n /** Its further paragraphs, each its own line. */\n more?: string[]\n /** Its bullet points (a plan's features). */\n list?: string[]\n /** The mark each bullet carries, as a path on the 24-unit square. */\n listIconPath?: string\n /** Its own calls to action, last in it. */\n links?: AiLayoutLink[]\n /** The picture over it, from the site's library. */\n picture?: AiLayoutPicture\n}\n\n/** A link of a page brought in (AGL-4107): its words and where it goes, `page:<id>`, `https:`, `mailto:` or `tel:`. */\nexport interface AiLayoutLink {\n text: string\n to?: string\n style?: AiLayoutStyle\n}\n\n/** A picture of a page brought in (AGL-4107): the library's copy of the source's own. */\nexport interface AiLayoutPicture {\n src: string\n alt: string\n /** Its width over its height, where the library knows them. */\n ratio?: number\n /** Where pressing it goes. */\n to?: string\n /** A byline's portrait beside a few words, drawn small. */\n small?: boolean\n}\n\n/**\n * The block kinds only a page brought in draws (AGL-4107), never in a tool\n * schema: a TABLE of rows of cells, TABS whose panels hold blocks, and a\n * strip of LOGOS — pictures, or short words as chips.\n */\nexport const AI_LAYOUT_IMPORT_BLOCK_KINDS = ['table', 'tabs', 'logos'] as const\nexport type AiLayoutImportBlockKind = (typeof AI_LAYOUT_IMPORT_BLOCK_KINDS)[number]\n\n/** A tab of a page brought in: its label and the blocks its panel shows. */\nexport interface AiLayoutTab {\n label: string\n /** The tab the source shows open. */\n selected?: boolean\n blocks: AiLayoutBlock[]\n}\n\nexport interface AiLayoutBlock {\n kind: AiLayoutBlockKind | AiLayoutImportBlockKind\n col?: number\n text?: string\n to?: string\n style?: AiLayoutStyle\n icon?: string\n items?: AiLayoutItem[]\n // ── A page brought in (AGL-4107), never in a tool schema ──\n /** An image block's picture, the source's own. */\n picture?: AiLayoutPicture\n /** A table's rows, each its cells' words; `header` when its first row names the columns. */\n rows?: Array<{ cells: string[] }>\n header?: boolean\n /** A tab strip's tabs. */\n tabs?: AiLayoutTab[]\n /** How a group is drawn, as the page's design answer chose (`submit_design`). */\n variant?: AiLayoutGroupStyle\n /** The columns the source lays a group's items out in on a wide screen. */\n columns?: number\n}\n\n/** How a group of a page brought in is drawn (AGL-4107): the compiler's group variants a source can show. */\nexport const AI_LAYOUT_GROUP_STYLES = ['cards', 'ruled', 'icons', 'pictures', 'articles'] as const\nexport type AiLayoutGroupStyle = (typeof AI_LAYOUT_GROUP_STYLES)[number]\n\n/** The heroes a page brought in may open with (AGL-4107): each lays the source's own picture beside, behind or under its words. */\nexport const AI_LAYOUT_IMPORT_HEROES = ['split', 'bleed', 'cover', 'poster', 'column', 'editorial', 'panel'] as const\nexport type AiLayoutImportHero = (typeof AI_LAYOUT_IMPORT_HEROES)[number]\n\nexport interface AiLayoutSection {\n band?: AiLayoutBand\n cols?: number[]\n align?: AiLayoutAlign\n blocks: AiLayoutBlock[]\n /** A page brought in (AGL-4107): the hero its first section opens as, around the source's picture. */\n hero?: AiLayoutImportHero\n}\n\n/** A page: its plan's sections, in order. */\nexport interface AiLayoutPageDoc {\n sections: AiLayoutSection[]\n}\n\n/** A site's frame: the header and the footer every page renders between. */\nexport interface AiLayoutFrameDoc {\n header: AiLayoutSection\n footer: AiLayoutSection\n}\n\nconst ITEM_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['title', 'text', 'to', 'icon'],\n properties: {\n title: { type: 'string' },\n text: { type: 'string' },\n to: { type: 'string' },\n icon: { type: 'string' },\n },\n} as const\n\nconst BLOCK_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'col', 'text', 'to', 'style', 'icon', 'items'],\n properties: {\n kind: { type: 'string', enum: [...AI_LAYOUT_BLOCK_KINDS] },\n col: { type: 'integer' },\n text: { type: 'string' },\n to: { type: 'string' },\n style: { type: 'string', enum: [...AI_LAYOUT_STYLES, 'none'] },\n icon: { type: 'string' },\n items: { type: 'array', items: ITEM_SCHEMA },\n },\n} as const\n\nconst SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['band', 'cols', 'align', 'blocks'],\n properties: {\n band: { type: 'string', enum: [...AI_LAYOUT_BANDS] },\n cols: { type: 'array', items: { type: 'integer' } },\n align: { type: 'string', enum: [...AI_LAYOUT_ALIGNS] },\n blocks: { type: 'array', items: BLOCK_SCHEMA },\n },\n} as const\n\n/** The page tool: every section of one page, in plan order, in one answer. */\nexport const AI_LAYOUT_PAGE_TOOL: AiTool = {\n name: 'submit_page',\n description:\n 'Submit the page: one entry in sections for each planned section, in order, in the layout language.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: SECTION_SCHEMA } },\n },\n}\n\n/** The frame tool: the site's header and footer, in the same language. */\nexport const AI_LAYOUT_FRAME_TOOL: AiTool = {\n name: 'submit_frame',\n description:\n \"Submit the site's header and footer, each as a section in the layout language.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['header', 'footer'],\n properties: { header: SECTION_SCHEMA, footer: SECTION_SCHEMA },\n },\n}\n\n/**\n * The language as a model is taught it: the cached instructions both doors\n * share. Every word of copy is the model's; this says only how a design is\n * written down and what each word becomes.\n */\nexport const AI_LAYOUT_LANGUAGE_TEXT = [\n \"You design web pages in a compact layout language. You decide the structure and write every word; the platform turns your design into the page, with the site's theme, spacing and type, so never describe styling beyond the words below.\",\n '',\n 'A page is a list of sections, one for each section the plan names, in that order. A section has:',\n '- band: plain (the page background), soft (a light band), brand (the brand color) or dark (always dark). Vary the bands down a page so neighboring sections read apart; keep brand and dark to one or two sections a page.',\n '- cols: relative column widths for a row, such as [1, 1] for halves, [7, 5] for a wide and a narrow column, or [1, 1, 1]; at most 4. [] for one column.',\n '- align: start or center.',\n '- blocks: what the section shows, top to bottom. A block with col 0 or more goes in that column of the row (0 is the first); a block with col -1 spans the whole section, above the row when it comes first and below it when it comes last.',\n 'Every field is always written: a block that does not use one writes \"\" (text, to, icon), \"none\" (style), -1 (col) or [] (items); an item that does not use one writes \"\".',\n '',\n 'Block kinds:',\n \"- eyebrow: a short label above a heading. heading: a heading; the first heading of the page's first section is the page's title, and the first heading of every other section is that section's heading. lede: the larger sentence under a heading. text: a paragraph. note: small print.\",\n '- button: text is the label and to says where it goes; style primary, secondary or quiet. Two buttons in a row read as a pair.',\n '- image: text describes the picture to place, which becomes its alt text; the owner adds the picture. icon names a drawing to show until then. to lightbox opens it large when pressed.',\n '- list: items are the lines (title only; text may be empty).',\n \"- cards, steps, stats, faq: items, each with a title and a text. cards are features or services; steps are numbered; stats are a figure (title) with its label (text), only for figures the brief gives; faq items are a question (title) and its answer (text). Use no quotes block: the site has no customer's words to show.\",\n \"- form: places a saved form by its id in to. component: places a reusable component by its id in to; its items fill the component's props, title the prop name and text the value.\",\n '- style large on a heading makes the page title display-sized. icon (on a block or an item) is one of: ' +\n AI_ICON_WORDS.join(', ') +\n '.',\n '',\n \"Where a link goes (to): page:<page id> for a page of this site; #<n> for section n of this page (counted from 1); form for the site's form; phone or email to call or write to the phone number or email the brief gives; or an https: address the brief gives. A button the page has nowhere to send is left out. A to value is never written into words: in text, name a page by its title.\",\n '',\n \"Writing: plain, specific copy in the site's voice, from the brief, the business and its audience, never filler. Keep a page title under 70 characters and a section heading under 80. The site is published exactly as you write it, so where the brief leaves out a fact such as a price, a figure, a name, a phone number, an email, an address or opening hours, leave it out: write the sentence without it, and make no list line, card or contact row for it. Never write a placeholder or a gap in square brackets, and never invent the fact. Never invent a customer, a review, an award or a statistic.\",\n '',\n 'Design each page for this business and this request: choose the bands, the rows and the blocks that tell its story best, so no two sites read alike.',\n].join('\\n')\n\n// ── Reading an answer ────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value : undefined\n}\n\nfunction oneOf<T extends string>(\n value: unknown,\n values: readonly T[],\n): T | undefined {\n return typeof value === 'string' &&\n (values as readonly string[]).includes(value)\n ? (value as T)\n : undefined\n}\n\n/** What the reader settled on its own, for the job's record and the specs. */\nexport interface AiLayoutSettlement {\n /** Where: `sections[2].blocks[1]`, `header`. */\n at: string\n what: string\n}\n\nfunction readItem(raw: unknown): AiLayoutItem | null {\n if (!isRecord(raw)) return null\n const item: AiLayoutItem = {\n title: typeof raw['title'] === 'string' ? raw['title'] : '',\n text: typeof raw['text'] === 'string' ? raw['text'] : '',\n }\n if (!item.title.trim() && !item.text.trim()) return null\n const to = text(raw['to'])\n const icon = text(raw['icon'])\n return { ...item, ...(to ? { to } : {}), ...(icon ? { icon } : {}) }\n}\n\nfunction readBlock(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutBlock | null {\n if (!isRecord(raw)) {\n settled.push({ at, what: 'not a block; left out' })\n return null\n }\n const kind = oneOf(raw['kind'], AI_LAYOUT_BLOCK_KINDS)\n if (!kind) {\n settled.push({\n at,\n what: `unknown kind ${JSON.stringify(raw['kind'] ?? null)}; left out`,\n })\n return null\n }\n const block: AiLayoutBlock = { kind }\n if (\n typeof raw['col'] === 'number' &&\n Number.isInteger(raw['col']) &&\n raw['col'] >= 0\n )\n block.col = raw['col']\n const words = text(raw['text'])\n if (words) block.text = words\n const to = text(raw['to'])\n if (to) block.to = to.trim()\n const style = oneOf(raw['style'], AI_LAYOUT_STYLES)\n if (style) block.style = style\n const icon = text(raw['icon'])\n if (icon) block.icon = icon.trim().toLowerCase()\n if (AI_LAYOUT_GROUP_KINDS.has(kind) || kind === 'component') {\n const items = (Array.isArray(raw['items']) ? raw['items'] : [])\n .map(readItem)\n .filter((item): item is AiLayoutItem => !!item)\n const most =\n kind === 'quotes'\n ? AI_LAYOUT_MAX_QUOTES\n : kind === 'component'\n ? Infinity\n : AI_LAYOUT_MAX_ITEMS\n if (items.length > most)\n settled.push({\n at,\n what: `${items.length} items; the first ${most} kept`,\n })\n if (items.length) block.items = items.slice(0, most)\n }\n // A block with nothing to show is no block: a group with no items, words\n // with no words. A form or a component may name its record alone.\n const empty = AI_LAYOUT_GROUP_KINDS.has(kind)\n ? !block.items?.length\n : kind === 'form' || kind === 'component'\n ? false\n : !block.text\n if (empty) {\n settled.push({ at, what: `an empty ${kind}; left out` })\n return null\n }\n return block\n}\n\n/** A section read from an answer, or `null` with why it could not be. */\nexport function aiReadLayoutSection(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutSection = { blocks: [] }\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n if (Array.isArray(raw['cols'])) {\n const cols = raw['cols'].filter(\n (value): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0,\n )\n if (cols.length > AI_LAYOUT_MAX_COLS) {\n settled.push({\n at: `${at}.cols`,\n what: `${cols.length} columns; the first ${AI_LAYOUT_MAX_COLS} kept`,\n })\n }\n if (cols.length >= 2)\n section.cols = cols\n .slice(0, AI_LAYOUT_MAX_COLS)\n .map((value) => Math.min(12, Math.max(1, Math.round(value))))\n }\n const blocks = Array.isArray(raw['blocks']) ? raw['blocks'] : []\n blocks.forEach((entry, index) => {\n const block = readBlock(entry, `${at}.blocks[${index}]`, settled)\n if (!block) return\n if (\n block.col !== undefined &&\n section.cols &&\n block.col >= section.cols.length\n ) {\n settled.push({\n at: `${at}.blocks[${index}]`,\n what: `column ${block.col} of ${section.cols.length}; placed in the last`,\n })\n block.col = section.cols.length - 1\n }\n if (block.col !== undefined && !section.cols) delete block.col\n section.blocks.push(block)\n })\n return section.blocks.length ? section : null\n}\n\n/** A page answer read: the sections it gave, by plan position, and the positions it could not. */\nexport interface AiLayoutPageReading {\n /** One entry per planned section; `null` where the answer gave nothing usable. */\n sections: Array<AiLayoutSection | null>\n settled: AiLayoutSettlement[]\n}\n\n/** What a planned section is, as the reader matches an answer's sections to it. */\nexport interface AiLayoutPlannedSection {\n name: string\n items: number\n /** What the plan places in it: a form it binds there is one of these. */\n uses?: readonly string[]\n}\n\n/** A planned section's name that asks for a form: a contact, an inquiry, a booking. */\nconst FORM_SECTION =\n /\\b(form|contact|get in touch|reach|write|message|inquir(?:y|ies|e)|enquir(?:y|ies|e)|book(?:ing|ings)?|reserv(?:e|ation|ations)|quote|appointments?|rsvp|sign ?up|subscribe|newsletter|apply|application|register)\\b/i\n\n/** Whether a planned section is one a form may fill: it names one, or the plan binds a form there. */\nfunction takesForm(planned: AiLayoutPlannedSection): boolean {\n return FORM_SECTION.test(planned.name) || (planned.uses ?? []).some((ref) => /\\bform\\b/i.test(ref))\n}\n\n/** What a form's section scores against a planned section that asks for no form: never chosen while another fits. */\nconst AI_LAYOUT_FORM_MISFIT = -100\n\n/**\n * A word as a heading and a plan's name are compared: letters in any script,\n * with their accents (AGL-3791). As ASCII, \"Menú\" was \"men\", which every\n * \"mensajes\" began with, and \"galería\" was \"galer\" and \"a\".\n */\nconst WORD = /[\\p{L}\\p{M}\\p{N}]+/gu\nconst STOP = new Set(['a', 'an', 'and', 'the', 'of', 'for', 'to', 'our', 'your', 'with', 'in', 'on', 'we', 'us', 'you'])\nconst wordsOf = (text: string) => new Set((text.toLowerCase().match(WORD) ?? []).filter((word) => !STOP.has(word)))\n\n/** The kinds that show a section's repeated items. */\nconst ITEM_KINDS = new Set(['list', 'cards', 'steps', 'stats', 'quotes', 'faq', 'component'])\n\n/**\n * How well an answer's section fits a planned one: the words its headings\n * share with the plan's name, and whether it shows items where the plan\n * asked for them and none where it asked for none.\n *\n * A shared heading word outweighs the items (AGL-3660): a live portfolio's\n * plan gave \"Works Gallery\" no items, and its \"Selected works\" cards lost the\n * place to an inquiry form that shared no word with it. A section holding a\n * form fills only a planned section that asks for one — by its name, or a\n * form the plan binds there — so the form is never a page's stand-in.\n */\nfunction fit(raw: unknown, planned: AiLayoutPlannedSection): number {\n const blocks = isRecord(raw) && Array.isArray(raw['blocks']) ? raw['blocks'].filter(isRecord) : []\n const said = wordsOf(\n blocks\n .filter((block) => block['kind'] === 'heading' || block['kind'] === 'eyebrow')\n .map((block) => String(block['text'] ?? ''))\n .join(' '),\n )\n const named = wordsOf(planned.name)\n let shared = 0\n for (const word of named) if (said.has(word) || [...said].some((other) => other.startsWith(word) || word.startsWith(other))) shared += 1\n const showsItems = blocks.some((block) => ITEM_KINDS.has(String(block['kind'])) && (block['kind'] === 'component' || (Array.isArray(block['items']) && block['items'].length > 0)))\n const wantsItems = planned.items > 0\n const holdsForm = blocks.some((block) => block['kind'] === 'form')\n if (holdsForm && !takesForm(planned)) return AI_LAYOUT_FORM_MISFIT\n return shared * 2 + (wantsItems === showsItems ? 0.5 : -0.5)\n}\n\n/**\n * The answer's sections that best match the plan, in order, where it gave\n * more than the plan names (AGL-3660): a model that opened a page with a\n * hero of its own, or closed it with a call to action, shifted every planned\n * section by one, and the cut fell on the plan's last sections — the ones\n * that carried its items. Each planned section takes the answer's section\n * that fits it best, keeping the answer's order.\n */\nfunction alignSections(given: readonly unknown[], planned: readonly AiLayoutPlannedSection[]): number[] {\n const n = given.length\n const m = planned.length\n // best[i][j]: the best score placing plan sections j.. on answer sections i..\n const best: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(-Infinity))\n for (let i = 0; i <= n; i += 1) best[i][m] = 0\n for (let i = n - 1; i >= 0; i -= 1) {\n for (let j = m - 1; j >= 0; j -= 1) {\n if (n - i < m - j) continue\n best[i][j] = Math.max(best[i + 1][j], fit(given[i], planned[j]) + best[i + 1][j + 1])\n }\n }\n const picked: number[] = []\n let i = 0\n for (let j = 0; j < m; j += 1) {\n while (i < n && best[i][j] !== fit(given[i], planned[j]) + best[i + 1][j + 1]) i += 1\n picked.push(i)\n i += 1\n }\n return picked\n}\n\n/**\n * The page an answer describes, against the sections its plan names: each\n * planned section takes the answer's section at its position — or, where the\n * answer gave more sections than the plan names and the plan's sections are\n * given, the one that fits it best (`alignSections`) — an answer's extra\n * sections are left out, and a planned section the answer left empty or out\n * is `null` — the one thing a re-ask asks for again.\n */\nexport function aiReadLayoutPage(\n raw: unknown,\n planned: number,\n plan?: readonly AiLayoutPlannedSection[],\n): AiLayoutPageReading {\n const settled: AiLayoutSettlement[] = []\n const given =\n isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n const aligned =\n given.length > planned && plan && plan.length === planned\n ? alignSections(given, plan)\n : Array.from({ length: planned }, (_, index) => index)\n if (given.length > planned) {\n settled.push({\n at: 'sections',\n what: `${given.length} sections for a plan of ${planned}; ${\n aligned.every((at, index) => at === index) ? 'the extra left out' : `sections ${aligned.map((at) => at + 1).join(', ')} kept as the plan's`\n }`,\n })\n }\n const sections = aligned.map((at, index) =>\n aiReadLayoutSection(given[at], `sections[${index}]`, settled),\n )\n return { sections, settled }\n}\n\n/** A frame answer read: a header and a footer, either `null` where it was not usable. */\nexport function aiReadLayoutFrame(raw: unknown): {\n header: AiLayoutSection | null\n footer: AiLayoutSection | null\n settled: AiLayoutSettlement[]\n} {\n const settled: AiLayoutSettlement[] = []\n const record = isRecord(raw) ? raw : {}\n // A header needs no block of its own: its brand and navigation are the\n // site's, so an empty one is a plain bar.\n const header = isRecord(record['header'])\n ? (aiReadLayoutSection(record['header'], 'header', settled) ??\n readBare(record['header']))\n : null\n const footer = aiReadLayoutSection(record['footer'], 'footer', settled)\n return { header, footer, settled }\n}\n\n/** A section with no usable block, keeping only its band and alignment. */\nfunction readBare(raw: Record<string, unknown>): AiLayoutSection {\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n return { ...(band ? { band } : {}), ...(align ? { align } : {}), blocks: [] }\n}\n\n// ── The design of a page brought in (AGL-4107) ───────────────────────────\n\n/**\n * A page brought in from its owner's own website is laid out as its source\n * lays it out (AGL-4107): the reader reads its sections, bands, rows, groups,\n * pictures and icons (`site-import/site-import-layout.ts`), and code compiles\n * that structure with every word as written (`jobs/ai-site-import-compile.ts`).\n * The one question left for a model is the page's DESIGN, over the structure\n * it is shown: which hero the opening becomes around its picture, how each\n * group is drawn, a band where the source gave none, the kind of a group the\n * reader could not tell, and an alignment. The schema below names only those\n * choices, so no word of copy, no line and no structure has anywhere to go.\n */\n\n/** A section's choice where the source's own stands: its band, its alignment, a group's kind. */\nexport const AI_LAYOUT_DESIGN_SOURCE = 'source'\n\n/** The kinds a group the reader could not tell may be read as. */\nexport const AI_LAYOUT_DESIGN_GROUP_KINDS = ['cards', 'steps', 'stats', 'list'] as const\nexport type AiLayoutDesignGroupKind = (typeof AI_LAYOUT_DESIGN_GROUP_KINDS)[number]\n\n/** The bands a design answer may give a section the source drew on no band of its own. */\nexport const AI_LAYOUT_DESIGN_BANDS = ['plain', 'soft', 'dark'] as const\n\nexport interface AiLayoutDesignGroup {\n /** The group's kind, where the answer reads it otherwise than the reader did. */\n kind?: AiLayoutDesignGroupKind\n style?: AiLayoutGroupStyle\n}\n\nexport interface AiLayoutDesignSection {\n /** The hero the opening becomes, or `none` to keep it as the source lays it out. */\n hero?: AiLayoutImportHero | 'none'\n band?: (typeof AI_LAYOUT_DESIGN_BANDS)[number]\n align?: AiLayoutAlign\n /** One for each group of the section, in order. */\n groups: AiLayoutDesignGroup[]\n}\n\n/** A design answer read: one entry per section of the page, `null` where it gave none. */\nexport interface AiLayoutDesignAnswer {\n sections: Array<AiLayoutDesignSection | null>\n}\n\nconst DESIGN_GROUP_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'style'],\n properties: {\n kind: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_DESIGN_GROUP_KINDS] },\n style: { type: 'string', enum: [...AI_LAYOUT_GROUP_STYLES] },\n },\n} as const\n\nconst DESIGN_SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['hero', 'band', 'align', 'groups'],\n properties: {\n hero: { type: 'string', enum: ['none', ...AI_LAYOUT_IMPORT_HEROES] },\n band: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_DESIGN_BANDS] },\n align: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_ALIGNS] },\n groups: { type: 'array', items: DESIGN_GROUP_SCHEMA },\n },\n} as const\n\n/** The design tool: one entry per section of a page brought in, in order, naming design choices only. */\nexport const AI_LAYOUT_DESIGN_TOOL: AiTool = {\n name: 'submit_design',\n description: \"Submit the page's design: one entry in sections for each section of the page, in order, with one entry in groups for each of its groups.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: DESIGN_SECTION_SCHEMA } },\n },\n}\n\nfunction readDesignSection(raw: unknown): AiLayoutDesignSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutDesignSection = { groups: [] }\n const hero = oneOf(raw['hero'], ['none', ...AI_LAYOUT_IMPORT_HEROES] as const)\n if (hero) section.hero = hero\n const band = oneOf(raw['band'], AI_LAYOUT_DESIGN_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n for (const entry of Array.isArray(raw['groups']) ? raw['groups'] : []) {\n const group: AiLayoutDesignGroup = {}\n if (isRecord(entry)) {\n const kind = oneOf(entry['kind'], AI_LAYOUT_DESIGN_GROUP_KINDS)\n if (kind) group.kind = kind\n const style = oneOf(entry['style'], AI_LAYOUT_GROUP_STYLES)\n if (style) group.style = style\n }\n section.groups.push(group)\n }\n return section\n}\n\n/**\n * A design answer read (AGL-4107): each section's choices by position, and\n * `null` for one it gave nothing usable for, which is then drawn as its\n * source has it. An answer carries no words, and none would be read.\n */\nexport function aiReadLayoutDesign(raw: unknown, sections: number): AiLayoutDesignAnswer {\n const given = isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n return { sections: Array.from({ length: Math.max(0, sections) }, (_, index) => readDesignSection(given[index])) }\n}\n"],"names":["AI_ICON_WORDS","AI_LAYOUT_BLOCK_KINDS","AI_LAYOUT_GROUP_KINDS","Set","AI_LAYOUT_BANDS","AI_LAYOUT_ALIGNS","AI_LAYOUT_STYLES","AI_LAYOUT_MAX_COLS","AI_LAYOUT_MAX_ITEMS","AI_LAYOUT_MAX_QUOTES","AI_LAYOUT_IMPORT_BLOCK_KINDS","AI_LAYOUT_GROUP_STYLES","AI_LAYOUT_IMPORT_HEROES","ITEM_SCHEMA","type","additionalProperties","required","properties","title","text","to","icon","BLOCK_SCHEMA","kind","enum","col","style","items","SECTION_SCHEMA","band","cols","align","blocks","AI_LAYOUT_PAGE_TOOL","name","description","strict","inputSchema","sections","AI_LAYOUT_FRAME_TOOL","header","footer","AI_LAYOUT_LANGUAGE_TEXT","join","isRecord","value","Array","isArray","trim","undefined","oneOf","values","includes","readItem","raw","item","readBlock","at","settled","block","push","what","JSON","stringify","Number","isInteger","words","toLowerCase","has","map","filter","most","Infinity","length","slice","empty","aiReadLayoutSection","section","isFinite","Math","min","max","round","forEach","entry","index","FORM_SECTION","takesForm","planned","test","uses","some","ref","AI_LAYOUT_FORM_MISFIT","WORD","STOP","wordsOf","match","word","ITEM_KINDS","fit","said","String","named","shared","other","startsWith","showsItems","wantsItems","holdsForm","alignSections","given","n","m","best","from","fill","i","j","picked","aiReadLayoutPage","plan","aligned","_","every","aiReadLayoutFrame","record","readBare","AI_LAYOUT_DESIGN_SOURCE","AI_LAYOUT_DESIGN_GROUP_KINDS","AI_LAYOUT_DESIGN_BANDS","DESIGN_GROUP_SCHEMA","DESIGN_SECTION_SCHEMA","hero","groups","AI_LAYOUT_DESIGN_TOOL","readDesignSection","group","aiReadLayoutDesign"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-language.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 { AiTool } from '../providers/contract'\nimport { AI_ICON_WORDS } from '../runtime/ai-icon-library'\n\n/**\n * The compact layout language (AGL-3660): what a model designs a page, and a\n * site's header and footer, in — and the one place its shape is declared.\n *\n * ── Why a language, not a tree ───────────────────────────────────────────\n *\n * A page used to be asked for as a raw element tree, packed as escaped JSON\n * text inside a tool call and held to the doctrine's building rules: Grid\n * items sized as strings, every child attached, one h1, a destination on\n * every button, headings under their ceiling. A real model kept about half\n * of a page's sections on the first try, and a Free section was cut off at\n * its token ceiling. The founder chose a language over a library of section\n * templates (10/7): every page is designed for its own request, so no two\n * sites come out alike, and the rules hold because CODE writes the tree.\n *\n * So the model says WHAT each section is and how it is arranged, in a few\n * words of structure, and writes every word a visitor reads; the compiler\n * (`ai-layout-compiler.ts`) turns that into the element tree with the\n * palette's elements and the theme's tokens, by construction.\n *\n * ── The shape ────────────────────────────────────────────────────────────\n *\n * A page is its plan's sections, in order. A SECTION is\n * - `band` — its background: `plain` (the page), `soft` (a paper band),\n * `brand` (the brand color) or `dark` (always dark);\n * - `cols` — a row of columns by relative width, as `[7, 5]` or `[1, 1, 1]`,\n * up to four; absent, one column;\n * - `align` — `start` or `center`;\n * - `blocks` — what it shows, top to bottom.\n *\n * A BLOCK is one thing a visitor reads or uses. Its `kind` is one of\n * `AI_LAYOUT_BLOCK_KINDS`; `col` places it in a column of the row (0 first),\n * and a block with no `col` spans the section — above the row when it comes\n * before the first placed block, below it when it comes after. `text` is its\n * words, `to` where it goes, `style` its emphasis and `icon` a word from the\n * icon library. A group — `list`, `cards`, `steps`, `stats`, `quotes`,\n * `faq` — carries its `items`, each a `title`, a `text`, and optionally its\n * own `to` and `icon`.\n *\n * Where a block goes (`to`): `page:<id>` for a page of this site (its label\n * works too), `#<n>` for section n of this page, `form` for the site's form,\n * `phone` or `email` for the phone number or email the brief gives (a link\n * that calls or writes, AGL-3979), a form's or a component's id for a `form`\n * or `component` block, or an `https:` address the brief gave.\n *\n * A site's header and footer are two sections in the same language\n * (`AI_LAYOUT_FRAME_TOOL`): the header's brand and navigation are the\n * site's own and written by the compiler, so the model gives the header its\n * band, its alignment and an optional call-to-action button; the footer is\n * a section like any other.\n *\n * ── Cheap by shape ───────────────────────────────────────────────────────\n *\n * The schema is a structured object in the tool input (never JSON text in a\n * string), small enough for a strict tool (`tool-schema-limits.spec.ts`), and\n * the language's instructions ride in the cached prefix. A section is a few\n * dozen words of structure beside its copy, so a whole page is one answer.\n */\n\n/** Every kind of block, in the order the schema lists them. */\nexport const AI_LAYOUT_BLOCK_KINDS = [\n 'eyebrow',\n 'heading',\n 'lede',\n 'text',\n 'note',\n 'list',\n 'button',\n 'image',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n 'form',\n 'component',\n] as const\n\nexport type AiLayoutBlockKind = (typeof AI_LAYOUT_BLOCK_KINDS)[number]\n\n/** The kinds that carry `items`. */\nexport const AI_LAYOUT_GROUP_KINDS: ReadonlySet<string> = new Set<AiLayoutBlockKind>([\n 'list',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n])\n\nexport const AI_LAYOUT_BANDS = ['plain', 'soft', 'brand', 'dark'] as const\nexport type AiLayoutBand = (typeof AI_LAYOUT_BANDS)[number]\n\nexport const AI_LAYOUT_ALIGNS = ['start', 'center'] as const\nexport type AiLayoutAlign = (typeof AI_LAYOUT_ALIGNS)[number]\n\nexport const AI_LAYOUT_STYLES = [\n 'primary',\n 'secondary',\n 'quiet',\n 'large',\n] as const\nexport type AiLayoutStyle = (typeof AI_LAYOUT_STYLES)[number]\n\n/** The most columns a row holds; a fifth is folded into the fourth. */\nexport const AI_LAYOUT_MAX_COLS = 4\n\n/**\n * The most items a group shows: one under the eight copies rule 8 reads as a\n * list typed out by hand, which belongs in a dataset.\n */\nexport const AI_LAYOUT_MAX_ITEMS = 7\n\n/** The most quotes a group shows: each is a gap the owner fills with a real one. */\nexport const AI_LAYOUT_MAX_QUOTES = 3\n\nexport interface AiLayoutItem {\n title: string\n text: string\n to?: string\n icon?: string\n // ── A page brought in (AGL-4107): what the source's item shows beside its\n // title and text, every word as written. No model writes these: no tool\n // schema names them, and the compiler draws them only on a verbatim page.\n /** Its own icon, the source's drawing as a path on the 24-unit square. */\n iconPath?: string\n /** A figure that leads it: a step's number, a plan's price. */\n figure?: string\n /** Its further paragraphs, each its own line. */\n more?: string[]\n /** Its bullet points (a plan's features). */\n list?: string[]\n /** The mark each bullet carries, as a path on the 24-unit square. */\n listIconPath?: string\n /** Its own calls to action, last in it. */\n links?: AiLayoutLink[]\n /** The picture over it, from the site's library. */\n picture?: AiLayoutPicture\n}\n\n/** A link of a page brought in (AGL-4107): its words and where it goes, `page:<id>`, `https:`, `mailto:` or `tel:`. */\nexport interface AiLayoutLink {\n text: string\n to?: string\n style?: AiLayoutStyle\n}\n\n/** A picture of a page brought in (AGL-4107): the library's copy of the source's own. */\nexport interface AiLayoutPicture {\n src: string\n alt: string\n /** Its width over its height, where the library knows them. */\n ratio?: number\n /** Where pressing it goes. */\n to?: string\n /** A byline's portrait beside a few words, drawn small. */\n small?: boolean\n}\n\n/**\n * The block kinds only a page brought in draws (AGL-4107), never in a tool\n * schema: a TABLE of rows of cells, TABS whose panels hold blocks, and a\n * strip of LOGOS — pictures, or short words as chips.\n */\nexport const AI_LAYOUT_IMPORT_BLOCK_KINDS = ['table', 'tabs', 'logos'] as const\nexport type AiLayoutImportBlockKind = (typeof AI_LAYOUT_IMPORT_BLOCK_KINDS)[number]\n\n/** A tab of a page brought in: its label and the blocks its panel shows. */\nexport interface AiLayoutTab {\n label: string\n /** The tab the source shows open. */\n selected?: boolean\n blocks: AiLayoutBlock[]\n /**\n * The row of columns the source sets in the panel (AGL-4114), as a\n * section's `cols`: relative widths, its blocks placed by `col`.\n */\n cols?: number[]\n}\n\nexport interface AiLayoutBlock {\n kind: AiLayoutBlockKind | AiLayoutImportBlockKind\n col?: number\n text?: string\n to?: string\n style?: AiLayoutStyle\n icon?: string\n items?: AiLayoutItem[]\n // ── A page brought in (AGL-4107), never in a tool schema ──\n /** An image block's picture, the source's own. */\n picture?: AiLayoutPicture\n /** A table's rows, each its cells' words; `header` when its first row names the columns. */\n rows?: Array<{ cells: string[] }>\n header?: boolean\n /** A tab strip's tabs. */\n tabs?: AiLayoutTab[]\n /** How a group is drawn, as the page's design answer chose (`submit_design`). */\n variant?: AiLayoutGroupStyle\n /** The columns the source lays a group's items out in on a wide screen. */\n columns?: number\n}\n\n/** How a group of a page brought in is drawn (AGL-4107): the compiler's group variants a source can show. */\nexport const AI_LAYOUT_GROUP_STYLES = ['cards', 'ruled', 'icons', 'pictures', 'articles'] as const\nexport type AiLayoutGroupStyle = (typeof AI_LAYOUT_GROUP_STYLES)[number]\n\n/** The heroes a page brought in may open with (AGL-4107): each lays the source's own picture beside, behind or under its words. */\nexport const AI_LAYOUT_IMPORT_HEROES = ['split', 'bleed', 'cover', 'poster', 'column', 'editorial', 'panel'] as const\nexport type AiLayoutImportHero = (typeof AI_LAYOUT_IMPORT_HEROES)[number]\n\nexport interface AiLayoutSection {\n band?: AiLayoutBand\n cols?: number[]\n align?: AiLayoutAlign\n blocks: AiLayoutBlock[]\n /** A page brought in (AGL-4107): the hero its first section opens as, around the source's picture. */\n hero?: AiLayoutImportHero\n}\n\n/** A page: its plan's sections, in order. */\nexport interface AiLayoutPageDoc {\n sections: AiLayoutSection[]\n}\n\n/** A site's frame: the header and the footer every page renders between. */\nexport interface AiLayoutFrameDoc {\n header: AiLayoutSection\n footer: AiLayoutSection\n}\n\nconst ITEM_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['title', 'text', 'to', 'icon'],\n properties: {\n title: { type: 'string' },\n text: { type: 'string' },\n to: { type: 'string' },\n icon: { type: 'string' },\n },\n} as const\n\nconst BLOCK_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'col', 'text', 'to', 'style', 'icon', 'items'],\n properties: {\n kind: { type: 'string', enum: [...AI_LAYOUT_BLOCK_KINDS] },\n col: { type: 'integer' },\n text: { type: 'string' },\n to: { type: 'string' },\n style: { type: 'string', enum: [...AI_LAYOUT_STYLES, 'none'] },\n icon: { type: 'string' },\n items: { type: 'array', items: ITEM_SCHEMA },\n },\n} as const\n\nconst SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['band', 'cols', 'align', 'blocks'],\n properties: {\n band: { type: 'string', enum: [...AI_LAYOUT_BANDS] },\n cols: { type: 'array', items: { type: 'integer' } },\n align: { type: 'string', enum: [...AI_LAYOUT_ALIGNS] },\n blocks: { type: 'array', items: BLOCK_SCHEMA },\n },\n} as const\n\n/** The page tool: every section of one page, in plan order, in one answer. */\nexport const AI_LAYOUT_PAGE_TOOL: AiTool = {\n name: 'submit_page',\n description:\n 'Submit the page: one entry in sections for each planned section, in order, in the layout language.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: SECTION_SCHEMA } },\n },\n}\n\n/** The frame tool: the site's header and footer, in the same language. */\nexport const AI_LAYOUT_FRAME_TOOL: AiTool = {\n name: 'submit_frame',\n description:\n \"Submit the site's header and footer, each as a section in the layout language.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['header', 'footer'],\n properties: { header: SECTION_SCHEMA, footer: SECTION_SCHEMA },\n },\n}\n\n/**\n * The language as a model is taught it: the cached instructions both doors\n * share. Every word of copy is the model's; this says only how a design is\n * written down and what each word becomes.\n */\nexport const AI_LAYOUT_LANGUAGE_TEXT = [\n \"You design web pages in a compact layout language. You decide the structure and write every word; the platform turns your design into the page, with the site's theme, spacing and type, so never describe styling beyond the words below.\",\n '',\n 'A page is a list of sections, one for each section the plan names, in that order. A section has:',\n '- band: plain (the page background), soft (a light band), brand (the brand color) or dark (always dark). Vary the bands down a page so neighboring sections read apart; keep brand and dark to one or two sections a page.',\n '- cols: relative column widths for a row, such as [1, 1] for halves, [7, 5] for a wide and a narrow column, or [1, 1, 1]; at most 4. [] for one column.',\n '- align: start or center.',\n '- blocks: what the section shows, top to bottom. A block with col 0 or more goes in that column of the row (0 is the first); a block with col -1 spans the whole section, above the row when it comes first and below it when it comes last.',\n 'Every field is always written: a block that does not use one writes \"\" (text, to, icon), \"none\" (style), -1 (col) or [] (items); an item that does not use one writes \"\".',\n '',\n 'Block kinds:',\n \"- eyebrow: a short label above a heading. heading: a heading; the first heading of the page's first section is the page's title, and the first heading of every other section is that section's heading. lede: the larger sentence under a heading. text: a paragraph. note: small print.\",\n '- button: text is the label and to says where it goes; style primary, secondary or quiet. Two buttons in a row read as a pair.',\n '- image: text describes the picture to place, which becomes its alt text; the owner adds the picture. icon names a drawing to show until then. to lightbox opens it large when pressed.',\n '- list: items are the lines (title only; text may be empty).',\n \"- cards, steps, stats, faq: items, each with a title and a text. cards are features or services; steps are numbered; stats are a figure (title) with its label (text), only for figures the brief gives; faq items are a question (title) and its answer (text). Use no quotes block: the site has no customer's words to show.\",\n \"- form: places a saved form by its id in to. component: places a reusable component by its id in to; its items fill the component's props, title the prop name and text the value.\",\n '- style large on a heading makes the page title display-sized. icon (on a block or an item) is one of: ' +\n AI_ICON_WORDS.join(', ') +\n '.',\n '',\n \"Where a link goes (to): page:<page id> for a page of this site; #<n> for section n of this page (counted from 1); form for the site's form; phone or email to call or write to the phone number or email the brief gives; or an https: address the brief gives. A button the page has nowhere to send is left out. A to value is never written into words: in text, name a page by its title.\",\n '',\n \"Writing: plain, specific copy in the site's voice, from the brief, the business and its audience, never filler. Keep a page title under 70 characters and a section heading under 80. The site is published exactly as you write it, so where the brief leaves out a fact such as a price, a figure, a name, a phone number, an email, an address or opening hours, leave it out: write the sentence without it, and make no list line, card or contact row for it. Never write a placeholder or a gap in square brackets, and never invent the fact. Never invent a customer, a review, an award or a statistic.\",\n '',\n 'Design each page for this business and this request: choose the bands, the rows and the blocks that tell its story best, so no two sites read alike.',\n].join('\\n')\n\n// ── Reading an answer ────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value : undefined\n}\n\nfunction oneOf<T extends string>(\n value: unknown,\n values: readonly T[],\n): T | undefined {\n return typeof value === 'string' &&\n (values as readonly string[]).includes(value)\n ? (value as T)\n : undefined\n}\n\n/** What the reader settled on its own, for the job's record and the specs. */\nexport interface AiLayoutSettlement {\n /** Where: `sections[2].blocks[1]`, `header`. */\n at: string\n what: string\n}\n\nfunction readItem(raw: unknown): AiLayoutItem | null {\n if (!isRecord(raw)) return null\n const item: AiLayoutItem = {\n title: typeof raw['title'] === 'string' ? raw['title'] : '',\n text: typeof raw['text'] === 'string' ? raw['text'] : '',\n }\n if (!item.title.trim() && !item.text.trim()) return null\n const to = text(raw['to'])\n const icon = text(raw['icon'])\n return { ...item, ...(to ? { to } : {}), ...(icon ? { icon } : {}) }\n}\n\nfunction readBlock(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutBlock | null {\n if (!isRecord(raw)) {\n settled.push({ at, what: 'not a block; left out' })\n return null\n }\n const kind = oneOf(raw['kind'], AI_LAYOUT_BLOCK_KINDS)\n if (!kind) {\n settled.push({\n at,\n what: `unknown kind ${JSON.stringify(raw['kind'] ?? null)}; left out`,\n })\n return null\n }\n const block: AiLayoutBlock = { kind }\n if (\n typeof raw['col'] === 'number' &&\n Number.isInteger(raw['col']) &&\n raw['col'] >= 0\n )\n block.col = raw['col']\n const words = text(raw['text'])\n if (words) block.text = words\n const to = text(raw['to'])\n if (to) block.to = to.trim()\n const style = oneOf(raw['style'], AI_LAYOUT_STYLES)\n if (style) block.style = style\n const icon = text(raw['icon'])\n if (icon) block.icon = icon.trim().toLowerCase()\n if (AI_LAYOUT_GROUP_KINDS.has(kind) || kind === 'component') {\n const items = (Array.isArray(raw['items']) ? raw['items'] : [])\n .map(readItem)\n .filter((item): item is AiLayoutItem => !!item)\n const most =\n kind === 'quotes'\n ? AI_LAYOUT_MAX_QUOTES\n : kind === 'component'\n ? Infinity\n : AI_LAYOUT_MAX_ITEMS\n if (items.length > most)\n settled.push({\n at,\n what: `${items.length} items; the first ${most} kept`,\n })\n if (items.length) block.items = items.slice(0, most)\n }\n // A block with nothing to show is no block: a group with no items, words\n // with no words. A form or a component may name its record alone.\n const empty = AI_LAYOUT_GROUP_KINDS.has(kind)\n ? !block.items?.length\n : kind === 'form' || kind === 'component'\n ? false\n : !block.text\n if (empty) {\n settled.push({ at, what: `an empty ${kind}; left out` })\n return null\n }\n return block\n}\n\n/** A section read from an answer, or `null` with why it could not be. */\nexport function aiReadLayoutSection(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutSection = { blocks: [] }\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n if (Array.isArray(raw['cols'])) {\n const cols = raw['cols'].filter(\n (value): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0,\n )\n if (cols.length > AI_LAYOUT_MAX_COLS) {\n settled.push({\n at: `${at}.cols`,\n what: `${cols.length} columns; the first ${AI_LAYOUT_MAX_COLS} kept`,\n })\n }\n if (cols.length >= 2)\n section.cols = cols\n .slice(0, AI_LAYOUT_MAX_COLS)\n .map((value) => Math.min(12, Math.max(1, Math.round(value))))\n }\n const blocks = Array.isArray(raw['blocks']) ? raw['blocks'] : []\n blocks.forEach((entry, index) => {\n const block = readBlock(entry, `${at}.blocks[${index}]`, settled)\n if (!block) return\n if (\n block.col !== undefined &&\n section.cols &&\n block.col >= section.cols.length\n ) {\n settled.push({\n at: `${at}.blocks[${index}]`,\n what: `column ${block.col} of ${section.cols.length}; placed in the last`,\n })\n block.col = section.cols.length - 1\n }\n if (block.col !== undefined && !section.cols) delete block.col\n section.blocks.push(block)\n })\n return section.blocks.length ? section : null\n}\n\n/** A page answer read: the sections it gave, by plan position, and the positions it could not. */\nexport interface AiLayoutPageReading {\n /** One entry per planned section; `null` where the answer gave nothing usable. */\n sections: Array<AiLayoutSection | null>\n settled: AiLayoutSettlement[]\n}\n\n/** What a planned section is, as the reader matches an answer's sections to it. */\nexport interface AiLayoutPlannedSection {\n name: string\n items: number\n /** What the plan places in it: a form it binds there is one of these. */\n uses?: readonly string[]\n}\n\n/** A planned section's name that asks for a form: a contact, an inquiry, a booking. */\nconst FORM_SECTION =\n /\\b(form|contact|get in touch|reach|write|message|inquir(?:y|ies|e)|enquir(?:y|ies|e)|book(?:ing|ings)?|reserv(?:e|ation|ations)|quote|appointments?|rsvp|sign ?up|subscribe|newsletter|apply|application|register)\\b/i\n\n/** Whether a planned section is one a form may fill: it names one, or the plan binds a form there. */\nfunction takesForm(planned: AiLayoutPlannedSection): boolean {\n return FORM_SECTION.test(planned.name) || (planned.uses ?? []).some((ref) => /\\bform\\b/i.test(ref))\n}\n\n/** What a form's section scores against a planned section that asks for no form: never chosen while another fits. */\nconst AI_LAYOUT_FORM_MISFIT = -100\n\n/**\n * A word as a heading and a plan's name are compared: letters in any script,\n * with their accents (AGL-3791). As ASCII, \"Menú\" was \"men\", which every\n * \"mensajes\" began with, and \"galería\" was \"galer\" and \"a\".\n */\nconst WORD = /[\\p{L}\\p{M}\\p{N}]+/gu\nconst STOP = new Set(['a', 'an', 'and', 'the', 'of', 'for', 'to', 'our', 'your', 'with', 'in', 'on', 'we', 'us', 'you'])\nconst wordsOf = (text: string) => new Set((text.toLowerCase().match(WORD) ?? []).filter((word) => !STOP.has(word)))\n\n/** The kinds that show a section's repeated items. */\nconst ITEM_KINDS = new Set(['list', 'cards', 'steps', 'stats', 'quotes', 'faq', 'component'])\n\n/**\n * How well an answer's section fits a planned one: the words its headings\n * share with the plan's name, and whether it shows items where the plan\n * asked for them and none where it asked for none.\n *\n * A shared heading word outweighs the items (AGL-3660): a live portfolio's\n * plan gave \"Works Gallery\" no items, and its \"Selected works\" cards lost the\n * place to an inquiry form that shared no word with it. A section holding a\n * form fills only a planned section that asks for one — by its name, or a\n * form the plan binds there — so the form is never a page's stand-in.\n */\nfunction fit(raw: unknown, planned: AiLayoutPlannedSection): number {\n const blocks = isRecord(raw) && Array.isArray(raw['blocks']) ? raw['blocks'].filter(isRecord) : []\n const said = wordsOf(\n blocks\n .filter((block) => block['kind'] === 'heading' || block['kind'] === 'eyebrow')\n .map((block) => String(block['text'] ?? ''))\n .join(' '),\n )\n const named = wordsOf(planned.name)\n let shared = 0\n for (const word of named) if (said.has(word) || [...said].some((other) => other.startsWith(word) || word.startsWith(other))) shared += 1\n const showsItems = blocks.some((block) => ITEM_KINDS.has(String(block['kind'])) && (block['kind'] === 'component' || (Array.isArray(block['items']) && block['items'].length > 0)))\n const wantsItems = planned.items > 0\n const holdsForm = blocks.some((block) => block['kind'] === 'form')\n if (holdsForm && !takesForm(planned)) return AI_LAYOUT_FORM_MISFIT\n return shared * 2 + (wantsItems === showsItems ? 0.5 : -0.5)\n}\n\n/**\n * The answer's sections that best match the plan, in order, where it gave\n * more than the plan names (AGL-3660): a model that opened a page with a\n * hero of its own, or closed it with a call to action, shifted every planned\n * section by one, and the cut fell on the plan's last sections — the ones\n * that carried its items. Each planned section takes the answer's section\n * that fits it best, keeping the answer's order.\n */\nfunction alignSections(given: readonly unknown[], planned: readonly AiLayoutPlannedSection[]): number[] {\n const n = given.length\n const m = planned.length\n // best[i][j]: the best score placing plan sections j.. on answer sections i..\n const best: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(-Infinity))\n for (let i = 0; i <= n; i += 1) best[i][m] = 0\n for (let i = n - 1; i >= 0; i -= 1) {\n for (let j = m - 1; j >= 0; j -= 1) {\n if (n - i < m - j) continue\n best[i][j] = Math.max(best[i + 1][j], fit(given[i], planned[j]) + best[i + 1][j + 1])\n }\n }\n const picked: number[] = []\n let i = 0\n for (let j = 0; j < m; j += 1) {\n while (i < n && best[i][j] !== fit(given[i], planned[j]) + best[i + 1][j + 1]) i += 1\n picked.push(i)\n i += 1\n }\n return picked\n}\n\n/**\n * The page an answer describes, against the sections its plan names: each\n * planned section takes the answer's section at its position — or, where the\n * answer gave more sections than the plan names and the plan's sections are\n * given, the one that fits it best (`alignSections`) — an answer's extra\n * sections are left out, and a planned section the answer left empty or out\n * is `null` — the one thing a re-ask asks for again.\n */\nexport function aiReadLayoutPage(\n raw: unknown,\n planned: number,\n plan?: readonly AiLayoutPlannedSection[],\n): AiLayoutPageReading {\n const settled: AiLayoutSettlement[] = []\n const given =\n isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n const aligned =\n given.length > planned && plan && plan.length === planned\n ? alignSections(given, plan)\n : Array.from({ length: planned }, (_, index) => index)\n if (given.length > planned) {\n settled.push({\n at: 'sections',\n what: `${given.length} sections for a plan of ${planned}; ${\n aligned.every((at, index) => at === index) ? 'the extra left out' : `sections ${aligned.map((at) => at + 1).join(', ')} kept as the plan's`\n }`,\n })\n }\n const sections = aligned.map((at, index) =>\n aiReadLayoutSection(given[at], `sections[${index}]`, settled),\n )\n return { sections, settled }\n}\n\n/** A frame answer read: a header and a footer, either `null` where it was not usable. */\nexport function aiReadLayoutFrame(raw: unknown): {\n header: AiLayoutSection | null\n footer: AiLayoutSection | null\n settled: AiLayoutSettlement[]\n} {\n const settled: AiLayoutSettlement[] = []\n const record = isRecord(raw) ? raw : {}\n // A header needs no block of its own: its brand and navigation are the\n // site's, so an empty one is a plain bar.\n const header = isRecord(record['header'])\n ? (aiReadLayoutSection(record['header'], 'header', settled) ??\n readBare(record['header']))\n : null\n const footer = aiReadLayoutSection(record['footer'], 'footer', settled)\n return { header, footer, settled }\n}\n\n/** A section with no usable block, keeping only its band and alignment. */\nfunction readBare(raw: Record<string, unknown>): AiLayoutSection {\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n return { ...(band ? { band } : {}), ...(align ? { align } : {}), blocks: [] }\n}\n\n// ── The design of a page brought in (AGL-4107) ───────────────────────────\n\n/**\n * A page brought in from its owner's own website is laid out as its source\n * lays it out (AGL-4107): the reader reads its sections, bands, rows, groups,\n * pictures and icons (`site-import/site-import-layout.ts`), and code compiles\n * that structure with every word as written (`jobs/ai-site-import-compile.ts`).\n * The one question left for a model is the page's DESIGN, over the structure\n * it is shown: which hero the opening becomes around its picture, how each\n * group is drawn, a band where the source gave none, the kind of a group the\n * reader could not tell, and an alignment. The schema below names only those\n * choices, so no word of copy, no line and no structure has anywhere to go.\n */\n\n/** A section's choice where the source's own stands: its band, its alignment, a group's kind. */\nexport const AI_LAYOUT_DESIGN_SOURCE = 'source'\n\n/** The kinds a group the reader could not tell may be read as. */\nexport const AI_LAYOUT_DESIGN_GROUP_KINDS = ['cards', 'steps', 'stats', 'list'] as const\nexport type AiLayoutDesignGroupKind = (typeof AI_LAYOUT_DESIGN_GROUP_KINDS)[number]\n\n/** The bands a design answer may give a section the source drew on no band of its own. */\nexport const AI_LAYOUT_DESIGN_BANDS = ['plain', 'soft', 'dark'] as const\n\nexport interface AiLayoutDesignGroup {\n /** The group's kind, where the answer reads it otherwise than the reader did. */\n kind?: AiLayoutDesignGroupKind\n style?: AiLayoutGroupStyle\n}\n\nexport interface AiLayoutDesignSection {\n /** The hero the opening becomes, or `none` to keep it as the source lays it out. */\n hero?: AiLayoutImportHero | 'none'\n band?: (typeof AI_LAYOUT_DESIGN_BANDS)[number]\n align?: AiLayoutAlign\n /** One for each group of the section, in order. */\n groups: AiLayoutDesignGroup[]\n}\n\n/** A design answer read: one entry per section of the page, `null` where it gave none. */\nexport interface AiLayoutDesignAnswer {\n sections: Array<AiLayoutDesignSection | null>\n}\n\nconst DESIGN_GROUP_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'style'],\n properties: {\n kind: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_DESIGN_GROUP_KINDS] },\n style: { type: 'string', enum: [...AI_LAYOUT_GROUP_STYLES] },\n },\n} as const\n\nconst DESIGN_SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['hero', 'band', 'align', 'groups'],\n properties: {\n hero: { type: 'string', enum: ['none', ...AI_LAYOUT_IMPORT_HEROES] },\n band: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_DESIGN_BANDS] },\n align: { type: 'string', enum: [AI_LAYOUT_DESIGN_SOURCE, ...AI_LAYOUT_ALIGNS] },\n groups: { type: 'array', items: DESIGN_GROUP_SCHEMA },\n },\n} as const\n\n/** The design tool: one entry per section of a page brought in, in order, naming design choices only. */\nexport const AI_LAYOUT_DESIGN_TOOL: AiTool = {\n name: 'submit_design',\n description: \"Submit the page's design: one entry in sections for each section of the page, in order, with one entry in groups for each of its groups.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: DESIGN_SECTION_SCHEMA } },\n },\n}\n\nfunction readDesignSection(raw: unknown): AiLayoutDesignSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutDesignSection = { groups: [] }\n const hero = oneOf(raw['hero'], ['none', ...AI_LAYOUT_IMPORT_HEROES] as const)\n if (hero) section.hero = hero\n const band = oneOf(raw['band'], AI_LAYOUT_DESIGN_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n for (const entry of Array.isArray(raw['groups']) ? raw['groups'] : []) {\n const group: AiLayoutDesignGroup = {}\n if (isRecord(entry)) {\n const kind = oneOf(entry['kind'], AI_LAYOUT_DESIGN_GROUP_KINDS)\n if (kind) group.kind = kind\n const style = oneOf(entry['style'], AI_LAYOUT_GROUP_STYLES)\n if (style) group.style = style\n }\n section.groups.push(group)\n }\n return section\n}\n\n/**\n * A design answer read (AGL-4107): each section's choices by position, and\n * `null` for one it gave nothing usable for, which is then drawn as its\n * source has it. An answer carries no words, and none would be read.\n */\nexport function aiReadLayoutDesign(raw: unknown, sections: number): AiLayoutDesignAnswer {\n const given = isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n return { sections: Array.from({ length: Math.max(0, sections) }, (_, index) => readDesignSection(given[index])) }\n}\n"],"names":["AI_ICON_WORDS","AI_LAYOUT_BLOCK_KINDS","AI_LAYOUT_GROUP_KINDS","Set","AI_LAYOUT_BANDS","AI_LAYOUT_ALIGNS","AI_LAYOUT_STYLES","AI_LAYOUT_MAX_COLS","AI_LAYOUT_MAX_ITEMS","AI_LAYOUT_MAX_QUOTES","AI_LAYOUT_IMPORT_BLOCK_KINDS","AI_LAYOUT_GROUP_STYLES","AI_LAYOUT_IMPORT_HEROES","ITEM_SCHEMA","type","additionalProperties","required","properties","title","text","to","icon","BLOCK_SCHEMA","kind","enum","col","style","items","SECTION_SCHEMA","band","cols","align","blocks","AI_LAYOUT_PAGE_TOOL","name","description","strict","inputSchema","sections","AI_LAYOUT_FRAME_TOOL","header","footer","AI_LAYOUT_LANGUAGE_TEXT","join","isRecord","value","Array","isArray","trim","undefined","oneOf","values","includes","readItem","raw","item","readBlock","at","settled","block","push","what","JSON","stringify","Number","isInteger","words","toLowerCase","has","map","filter","most","Infinity","length","slice","empty","aiReadLayoutSection","section","isFinite","Math","min","max","round","forEach","entry","index","FORM_SECTION","takesForm","planned","test","uses","some","ref","AI_LAYOUT_FORM_MISFIT","WORD","STOP","wordsOf","match","word","ITEM_KINDS","fit","said","String","named","shared","other","startsWith","showsItems","wantsItems","holdsForm","alignSections","given","n","m","best","from","fill","i","j","picked","aiReadLayoutPage","plan","aligned","_","every","aiReadLayoutFrame","record","readBare","AI_LAYOUT_DESIGN_SOURCE","AI_LAYOUT_DESIGN_GROUP_KINDS","AI_LAYOUT_DESIGN_BANDS","DESIGN_GROUP_SCHEMA","DESIGN_SECTION_SCHEMA","hero","groups","AI_LAYOUT_DESIGN_TOOL","readDesignSection","group","aiReadLayoutDesign"],"mappings":"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"}
@@ -95,7 +95,7 @@ const NOUNS = {
95
95
  */ export const AI_EDIT_NO_VERSIONS_DRAFT_COPY = 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.';
96
96
  /** The output's note: where the change is, that nothing visitors see moved, and what was left out. */ export function aiEditOutputNote(input) {
97
97
  const noun = NOUNS[input.kind];
98
- const where = input.placement === 'draft' ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open the ${noun} in the Besigner and choose Open draft to review it, then publish it when it is ready.` : input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
98
+ const where = input.placement === 'draft' ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open it in the Besigner to review it, then publish it when it is ready.` : input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
99
99
  const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : '';
100
100
  const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : '';
101
101
  return `${where}${versions}${leftOut}`.slice(0, 1000);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-job.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 { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_WHOLE_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - by default, on every plan, into the WORKING DRAFT of the version it\n * started from (AGL-4116) — the draft the Besigner saves to as an author\n * works and offers with Open draft, so the author reviews the change and\n * publishes it there exactly as they would their own. A draft the author\n * already has is built on, never replaced;\n * - when the request asks for a new version and the plan keeps version\n * history, as a NEW version beside the one it started from, which becomes\n * the page's current version only when the page is not published; a\n * published page's and a layout's current version never move. Asked for\n * on a plan without version history, the change goes into the draft and\n * the output says why.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n /** The request asked for a new version; otherwise the change goes into the draft. */\n newVersion: boolean\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n newVersion: inputs?.['newVersion'] === true,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. Written before AGL-4116. */\n | 'in-place'\n /** The working draft of the version it started from, which the Besigner offers with Open draft. */\n | 'draft'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/**\n * Why a change asked for as a new version went into the draft: the plan\n * keeps no version history. The versions route's own words for the plan.\n */\nexport const AI_EDIT_NO_VERSIONS_DRAFT_COPY =\n 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.'\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n /** A new version was asked for on a plan without version history. */\n versionsUnavailable?: boolean\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'draft'\n ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open the ${noun} in the Besigner and choose Open draft to review it, then publish it when it is ready.`\n : input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : ''\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${versions}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` with `whole` holds it to its limits): the\n * document root, then every element in the order it reads on the page — each\n * after its parent, so every ancestor chain the check walks is whole — with\n * its settings and its styles, up to the whole-document cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection and nobody to ask, so it is the WHOLE document: a\n * request about a picture five levels down is about an element the model can\n * see, and a restyle starts from the styles the element has. Each element\n * keeps its real child count and the document its real size, so the prompt\n * can say what a document past the cap leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = []\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Depth first, children in their stored order: the page as it reads.\n const stack: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n while (stack.length) {\n const at = stack.pop() as (typeof stack)[number]\n order.push(at)\n const children = childrenOf(nodes[at.id] as Record<string, unknown>)\n for (let index = children.length - 1; index >= 0; index -= 1) {\n const child = children[index]\n if (seen.has(child)) continue\n seen.add(child)\n stack.push({ id: child, parentId: at.id, index })\n }\n }\n return {\n selectedId: null,\n total: order.length,\n nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n ...(isRecord(node['sx']) ? { sx: node['sx'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_WHOLE_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","newVersion","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","AI_EDIT_NO_VERSIONS_DRAFT_COPY","aiEditOutputNote","input","noun","where","placement","versions","versionsUnavailable","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","seen","Set","stack","parentId","index","at","pop","push","children","child","has","add","selectedId","total","map","componentId","childCount","name","props","sx"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-job.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 { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_WHOLE_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - by default, on every plan, into the WORKING DRAFT of the version it\n * started from (AGL-4116) — the draft the Besigner saves to as an author\n * works and offers with Open draft, so the author reviews the change and\n * publishes it there exactly as they would their own. A draft the author\n * already has is built on, never replaced;\n * - when the request asks for a new version and the plan keeps version\n * history, as a NEW version beside the one it started from, which becomes\n * the page's current version only when the page is not published; a\n * published page's and a layout's current version never move. Asked for\n * on a plan without version history, the change goes into the draft and\n * the output says why.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n /** The request asked for a new version; otherwise the change goes into the draft. */\n newVersion: boolean\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n newVersion: inputs?.['newVersion'] === true,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. Written before AGL-4116. */\n | 'in-place'\n /** The working draft of the version it started from, which the Besigner offers with Open draft. */\n | 'draft'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/**\n * Why a change asked for as a new version went into the draft: the plan\n * keeps no version history. The versions route's own words for the plan.\n */\nexport const AI_EDIT_NO_VERSIONS_DRAFT_COPY =\n 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.'\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n /** A new version was asked for on a plan without version history. */\n versionsUnavailable?: boolean\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'draft'\n ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open it in the Besigner to review it, then publish it when it is ready.`\n : input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : ''\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${versions}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` with `whole` holds it to its limits): the\n * document root, then every element in the order it reads on the page — each\n * after its parent, so every ancestor chain the check walks is whole — with\n * its settings and its styles, up to the whole-document cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection and nobody to ask, so it is the WHOLE document: a\n * request about a picture five levels down is about an element the model can\n * see, and a restyle starts from the styles the element has. Each element\n * keeps its real child count and the document its real size, so the prompt\n * can say what a document past the cap leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = []\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Depth first, children in their stored order: the page as it reads.\n const stack: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n while (stack.length) {\n const at = stack.pop() as (typeof stack)[number]\n order.push(at)\n const children = childrenOf(nodes[at.id] as Record<string, unknown>)\n for (let index = children.length - 1; index >= 0; index -= 1) {\n const child = children[index]\n if (seen.has(child)) continue\n seen.add(child)\n stack.push({ id: child, parentId: at.id, index })\n }\n }\n return {\n selectedId: null,\n total: order.length,\n nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n ...(isRecord(node['sx']) ? { sx: node['sx'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_WHOLE_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","newVersion","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","AI_EDIT_NO_VERSIONS_DRAFT_COPY","aiEditOutputNote","input","noun","where","placement","versions","versionsUnavailable","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","seen","Set","stack","parentId","index","at","pop","push","children","child","has","add","selectedId","total","map","componentId","childCount","name","props","sx"],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { besignerOpenDraftHref } from "@aglyn/aglyn/app-utils/version-draft";
17
18
  import { aiBuildOutcomeLine, aiSitePartialCopy } from "./ai-build-progress.js";
18
19
  import { aiJobKindNoun } from "./ai-job-activity.js";
19
20
  import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
@@ -74,7 +75,8 @@ import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
74
75
  const versioned = own.find((output)=>AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId);
75
76
  if (own.length === 1 && versioned) {
76
77
  const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource];
77
- return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`;
78
+ const besigner = `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`;
79
+ return versioned.inDraft ? besignerOpenDraftHref(besigner) : besigner;
78
80
  }
79
81
  }
80
82
  return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`;
@@ -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":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,qBAAqB,QAAQ,uCAAsC;AAC5E,SAASC,kBAAkB,EAAEC,iBAAiB,QAAQ,yBAAqB;AAC3E,SAASC,aAAa,QAAQ,uBAAmB;AACjD,SAASC,kBAAkB,EAAEC,eAAe,QAAQ,2BAAuB;AAG3E,8DAA8D,GAC9D,OAAO,MAAMC,0BAA4E;IACvFC,QAAQ;IACRC,mBAAmB;IACnBC,QAAQ;IACRC,UAAU;IACV,uEAAuE;IACvE,sCAAsC;IACtCC,aAAa;AACf,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMC,oBAAoB,QAAO;AAExC,+EAA+E,GAC/E,OAAO,MAAMC,qBAAqB,WAAU;AAE5C;;;;CAIC,GACD,OAAO,MAAMC,0BACX,oGACA,uGAAsG;AAExG,+CAA+C,GAC/C,OAAO,SAASC,iBAAiBC,IAAe;IAC9C,MAAMC,OAAOd,cAAca;IAC3B,OAAOC,SAAS,WAAW,0BAA0B,CAAC,KAAK,EAAEA,KAAK,CAAC,EAAEA,KAAKC,QAAQ,CAAC,OAAO,QAAQ,KAAK,OAAO,CAAC;AACjH;AAyBA,4GAA4G,GAC5G,SAASC,WAAWC,GAAsB,EAAEC,IAAY,EAAEC,MAAiC;IACzF,MAAMC,SAASlB,gBAAgB;QAAEiB;QAAQE,iBAAiBJ,IAAII,eAAe;QAAEC,cAAcL,IAAIK,YAAY;IAAC;IAC9G,OAAOF,SAAS,GAAGF,KAAK,CAAC,EAAEE,QAAQ,GAAGF;AACxC;AAOA;;;;;;;;;;CAUC,GACD,OAAO,SAASK,gBAAgBN,GAAsB,EAAEO,EAAyB;IAC/E,MAAMC,OAAOR,IAAIS,MAAM,GAAG,CAAC,CAAC,EAAET,IAAIS,MAAM,EAAE,GAAG;IAC7C,4EAA4E;IAC5E,2DAA2D;IAC3D,4EAA4E;IAC5E,IAAI,AAACT,CAAAA,IAAIJ,IAAI,KAAK,UAAUI,IAAIJ,IAAI,KAAK,QAAO,KAAMI,IAAIS,MAAM,EAAE;QAChE,OAAO,GAAGD,OAAOf,mBAAmB,CAAC,EAAEiB,mBAAmBV,IAAIW,GAAG,GAAG;IACtE;IACA,IAAIJ,OAAO,UAAUP,IAAIS,MAAM,EAAE;YAClBT;QAAb,MAAMY,MAAM,EAACZ,eAAAA,IAAIa,OAAO,YAAXb,eAAe,EAAE,EAAEc,MAAM,CAAC,CAACC,SAAWA,OAAON,MAAM,KAAKT,IAAIS,MAAM;QAC/E,MAAMO,UAAUJ,IAAIE,MAAM,CAAC,CAACC,SAAWA,OAAOE,QAAQ,KAAK;QAC3D,IAAID,QAAQE,MAAM,GAAG,GAAG,OAAO,GAAGV,KAAK,QAAQ,CAAC;QAChD,MAAMW,YAAYP,IAAIQ,IAAI,CACxB,CAACL,SAAW7B,uBAAuB,CAAC6B,OAAOE,QAAQ,CAAC,IAAIF,OAAOM,SAAS;QAE1E,IAAIT,IAAIM,MAAM,KAAK,KAAKC,WAAW;YACjC,MAAMG,UAAUpC,uBAAuB,CAACiC,UAAUF,QAAQ,CAAC;YAC3D,MAAMM,WAAW,GAAGf,KAAK,CAAC,EAAEc,QAAQ,CAAC,EAAEH,UAAUK,EAAE,CAAC,UAAU,EAAEL,UAAUE,SAAS,CAAC,SAAS,CAAC;YAC9F,OAAOF,UAAUM,OAAO,GAAG7C,sBAAsB2C,YAAYA;QAC/D;IACF;IACA,OAAO,GAAGf,KAAK,CAAC,EAAEhB,kBAAkB,CAAC,EAAEkB,mBAAmBV,IAAIW,GAAG,GAAG;AACtE;AAcA,OAAO,SAASe,YACd1B,GAAsB,EACtBO,EAAyB,EACzBoB,OAA+B,IAAI;IAEnC,MAAM9B,OAAOd,cAAciB,IAAIJ,IAAI;IACnC,MAAMgC,QAAQ;QAAEC,OAAO7B,IAAI6B,KAAK;OAAM7B,IAAIS,MAAM,GAAG;QAAEA,QAAQT,IAAIS,MAAM;IAAC,IAAI,CAAC;IAC7E,MAAMqB,OAAOxB,gBAAgBN,KAAKO;IAClC,IAAIA,OAAO,gBAAgB;YAErBP,YAYAA,aAUAA,cAoBmBA;QA3CvB,gEAAgE;QAChE,KAAIA,aAAAA,IAAI+B,KAAK,qBAAT/B,WAAWgC,OAAO,EAAE;YACtB,OAAO;gBACLC,MAAM;gBACNC,OAAO;gBACPC,OAAO,CAAC,KAAK,EAAEtC,SAAS,WAAW,WAAWA,KAAK,0BAA0B,CAAC;gBAC9EuC,MAAM,GAAGpC,IAAI+B,KAAK,CAACC,OAAO,CAAC,gFAAgF,CAAC;gBAC5GF;eACGF;QAEP;QACA,yEAAyE;QACzE,qDAAqD;QACrD,IAAI5B,EAAAA,cAAAA,IAAIqC,MAAM,qBAAVrC,YAAYsC,MAAM,MAAK,QAAQ;YACjC,OAAO;gBACLL,MAAM;gBACNC,OAAO;gBACPC,OAAO;gBACPC,MAAMpC,IAAIqC,MAAM,CAACL,OAAO,IAAI;gBAC5BF;eACGF;QAEP;QACA,IAAI5B,EAAAA,eAAAA,IAAIqC,MAAM,qBAAVrC,aAAYsC,MAAM,MAAK,QAAQ;YACjC,OAAO;gBACLL,MAAM;gBACNC,OAAO;gBACPC,OACEtC,SAAS,WACL,4CACA,CAAC,KAAK,EAAEA,KAAK,mCAAmC,CAAC;gBACvDuC,MAAM;gBACNN;eACGF;QAEP;QACA,OAAO;YACLK,MAAM;YACNC,OAAO;YACPC,OAAO,CAAC,KAAK,EAAEtC,SAAS,WAAW,WAAW,GAAGA,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC;YACvE,8DAA8D;YAC9DuC,MAAMrC,WACJC,KACAhB,mBAAmBgB,EAAAA,eAAAA,IAAIqC,MAAM,qBAAVrC,aAAYgC,OAAO,KAAIhC,IAAIuC,KAAK,IAAI,sDAAsD;gBAAEC,MAAMxC,IAAIJ,IAAI,KAAK;YAAO,IACzI;YAEFkC;WACGF;IAEP;IACA,IAAIrB,OAAO,UAAU;QACnB,yEAAyE;QACzE,0EAA0E;QAC1E,OAAO;YACL0B,MAAM;YACNC,OAAO;YACPC,OAAOxC,iBAAiBK,IAAIJ,IAAI;YAChCwC,MAAM;gBAACpC,IAAIuC,KAAK;gBAAE7C;aAAwB,CAACoB,MAAM,CAAC2B,SAASC,IAAI,CAAC;YAChEZ;WACGF;IAEP;IACA,IAAIrB,OAAO,QAAQ;YAGHP,kBAoCCA;YAxB0BA;QAdzC,wEAAwE;QACxE,0EAA0E;QAC1E,MAAM2C,SAAQ3C,aAAAA,IAAI2C,KAAK,YAAT3C,aAAa,EAAE;QAC7B,MAAM4C,UAAUD,MAAME,IAAI,CAAC,CAACC,MAAQA,IAAI5C,MAAM,KAAK,YAAY4C,IAAI5C,MAAM,KAAK;QAC9E,MAAMgC,QAAQU,UAAU,YAAY;QACpC,MAAMG,UACJ/C,IAAIJ,IAAI,KAAK,SACTd,kBAAkB;YAAE6D;YAAOzC,QAAQ;QAAO,KAC1C0C,UACE/D,mBAAmB;YAAE8D;YAAOzC,QAAQ;QAAO,KAC3C;QACR,uEAAuE;QACvE,uEAAuE;QACvE,mCAAmC;QACnC,MAAM8C,YAAYhD,IAAIJ,IAAI,KAAK,kBAAUI,mBAAAA,IAAIiD,WAAW,qBAAfjD,iBAAiBgD,SAAS,CAAC9B,MAAM,mBAAI,IAAK;QACnF,IAAI8B,YAAY,GAAG;;gBACFhD;YAAf,MAAMkD,mBAASlD,oBAAAA,IAAIiD,WAAW,qBAAfjD,kBAAiBkD,MAAM,CAAChC,MAAM,oBAAI;YACjD,MAAMiC,SAASD,WAAW,IAAI,4BAA4B,GAAGA,OAAO,mBAAmB,CAAC;YACxF,qEAAqE;YACrE,sEAAsE;YACtE,4CAA4C;YAC5C,MAAME,OAAOzB,OAAO,GAAGA,KAAK0B,IAAI,CAAC,QAAQ,CAAC,GAAG;YAC7C,MAAMC,KAAK3B,CAAAA,wBAAAA,KAAM4B,GAAG,IAAG,CAAC,IAAI,EAAE5B,KAAK4B,GAAG,EAAE,GAAG;YAC3C,OAAO;gBACLtB,MAAM;gBACNC;gBACAC,OAAOY,UAAU,GAAGK,KAAK,6BAA6B,CAAC,GAAGA;gBAC1DhB,MAAMW,UACF,GAAGA,QAAQ,CAAC,EAAEG,SAAS,IAAI,CAAC,2BAA2B,EAAEI,GAAG,SAAS,EAAEH,OAAO,CAAC,CAAC,GAAG,CAAC,2BAA2B,EAAEG,GAAG,CAAC,CAAC,EAAE,GACxHJ,SAAS,IACP,CAAC,wBAAwB,EAAEI,GAAG,SAAS,EAAEH,OAAO,iCAAiC,CAAC,GAClFG,KACE,CAAC,wBAAwB,EAAEA,GAAG,sEAAsE,CAAC,GACrG;gBACRxB;eACGF;QAEP;QACA,MAAM4B,QAAQ,EAACxD,eAAAA,IAAIa,OAAO,YAAXb,eAAe,EAAE,EAAEc,MAAM,CAAC,CAACC,SAAWA,OAAOE,QAAQ,KAAK,UAAUC,MAAM;QACzF,MAAMiB,QACJnC,IAAIJ,IAAI,KAAK,UAAU4D,QAAQ,IAC3B,sCACAxD,IAAIJ,IAAI,KAAK,SACX,6BACAC,SAAS,WACP,yBACA,CAAC,KAAK,EAAEA,KAAK,SAAS,CAAC;QACjC,OAAO;YACLoC,MAAM;YACNC;YACAC;YACAC,MACEpC,IAAIJ,IAAI,KAAK,aAAaI,IAAIJ,IAAI,KAAK,SACnC,6BACA;gBAACmD;gBAAS;aAAoE,CAACjC,MAAM,CAAC2B,SAASC,IAAI,CAAC;YAC1GZ;WACGF;IAEP;IACA,OAAO;QACLK,MAAM;QACNC,OAAO;QACPC,OAAO,CAAC,KAAK,EAAEtC,SAAS,WAAW,WAAW,GAAGA,KAAK,IAAI,CAAC,CAAC,QAAQ,CAAC;QACrEuC,MAAMrC,WAAWC,KAAKhB,mBAAmBgB,IAAIuC,KAAK,IAAI,kCAAkC;YAAEC,MAAMxC,IAAIJ,IAAI,KAAK;QAAO,IAAI;QACxHkC;OACGF;AAEP;AAEA;;;;;;CAMC,GACD,OAAO,SAAS6B,wBAAwBzD,GAAsB,EAAE2B,OAA+B,IAAI;QAK7F3B;IAJJ,MAAMH,OAAOd,cAAciB,IAAIJ,IAAI;IACnC,MAAMgC,QAAQ;QAAEC,OAAO7B,IAAI6B,KAAK;OAAM7B,IAAIS,MAAM,GAAG;QAAEA,QAAQT,IAAIS,MAAM;IAAC,IAAI,CAAC;IAC7E,MAAMqB,OAAOxB,gBAAgBN,KAAK;IAClC,MAAM0D,KAAK/B,OAAO,CAAC,KAAK,EAAEA,KAAK0B,IAAI,EAAE,GAAG;IACxC,KAAIrD,aAAAA,IAAI+B,KAAK,qBAAT/B,WAAWgC,OAAO,EAAE;QACtB,OAAO;YACLC,MAAM;YACNC,OAAO;YACPC,OAAO,CAAC,KAAK,EAAEtC,OAAO6D,GAAG,gCAAgC,CAAC;YAC1DtB,MAAM,GAAGpC,IAAI+B,KAAK,CAACC,OAAO,CAAC,2GAA2G,CAAC;YACvIF;WACGF;IAEP;IACA,OAAO;QACLK,MAAM;QACNC,OAAO;QACPC,OAAOtC,SAAS,WAAW,CAAC,SAAS,EAAE6D,GAAG,yBAAyB,CAAC,GAAG,CAAC,KAAK,EAAE7D,KAAK,KAAK,EAAE6D,GAAG,yBAAyB,CAAC;QACxHtB,MAAM;QACNN;OACGF;AAEP"}