@aglyn/plugins-ai 1.0.0-beta.260 → 1.0.0-beta.262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
- package/src/lib/components/ai-brief-dialog.component.js +8 -132
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-follow.component.js +2 -35
- package/src/lib/components/ai-job-follow.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +2 -8
- package/src/lib/components/ai-job-plan.component.js +13 -74
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +43 -34
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
- package/src/lib/components/ai-template-collection-field.component.js +13 -10
- package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
- package/src/lib/components/ai-upsell-dialog.component.js +9 -66
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.d.ts +3 -8
- package/src/lib/components/use-ai-job-run.js +4 -15
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/insights/ai-figure-readers.js +15 -5
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
- package/src/lib/insights/ai-figure-summaries.js +79 -0
- package/src/lib/insights/ai-figure-summaries.js.map +1 -0
- package/src/lib/insights/ai-insight-readers.d.ts +2 -1
- package/src/lib/insights/ai-insight-readers.js +11 -26
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +6 -0
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +10 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -11
- package/src/lib/jobs/ai-layout-stock-photos.js +329 -32
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-photo-picker.js +13 -7
- package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.d.ts +35 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js +404 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-motion.d.ts +9 -3
- package/src/lib/layout-language/ai-layout-motion.js +9 -3
- package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
- package/src/lib/layout-language/ai-layout-pictures.js +12 -6
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-brief-copy.d.ts +85 -0
- package/src/lib/model/ai-brief-copy.js +152 -0
- package/src/lib/model/ai-brief-copy.js.map +1 -0
- package/src/lib/model/ai-create-view.d.ts +200 -0
- package/src/lib/model/ai-create-view.js +263 -0
- package/src/lib/model/ai-create-view.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +1 -1
- package/src/lib/model/ai-insight.js +4 -39
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +24 -0
- package/src/lib/model/ai-job-activity.js +52 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
- package/src/lib/model/ai-job-plan-lines.js +70 -0
- package/src/lib/model/ai-job-plan-lines.js.map +1 -0
- package/src/lib/model/ai-template-collections.d.ts +28 -0
- package/src/lib/model/ai-template-collections.js +28 -0
- package/src/lib/model/ai-template-collections.js.map +1 -0
- package/src/lib/model/ai-upsell-copy.d.ts +41 -0
- package/src/lib/model/ai-upsell-copy.js +85 -0
- package/src/lib/model/ai-upsell-copy.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
- package/src/lib/runtime/ai-doctrine-validators.js +148 -3
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +6 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
- package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +32 -0
- package/src/lib/server/ai-create-route.js +295 -0
- package/src/lib/server/ai-create-route.js.map +1 -0
- package/src/lib/server/ai-jobs-route.js +19 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,sCAAqC;AAEzE,SAASC,aAAa,EAAEC,8BAA8B,QAAQ,wBAAoB;AA6ElF,uEAAuE,GACvE,MAAMC,SACJ;AAEF;;;;;CAKC,GACD,SAASC,SAASC,KAAc;IAC9B,IAAIA,SAAS,OAAOA,UAAU,UAAU;QACtC,MAAMC,eAAeD;QACrB,KAAK,MAAME,cAAc;YAAC;YAAM;YAAM;YAAM;YAAM;SAAK,CAAE;YACvD,MAAMC,QAAQJ,SAASE,YAAY,CAACC,WAAW;YAC/C,IAAIC,UAAU,MAAM,OAAOA;QAC7B;QACA,OAAO;IACT;IACA,MAAMC,QAAQ,iDAAiDC,IAAI,CAACC,OAAON,gBAAAA,QAAS;IACpF,IAAI,CAACI,OAAO,OAAO;IACnB,MAAMD,QAAQI,OAAOH,KAAK,CAAC,EAAE,IAAIG,OAAOH,KAAK,CAAC,EAAE;IAChD,OAAOG,OAAOC,QAAQ,CAACL,UAAUA,QAAQ,IAAIA,QAAQ;AACvD;AAEA,gFAAgF,GAChF,MAAMM,eAAe,KAAK;AAE1B;;;;;CAKC,GACD,SAASC,WAAWC,KAAmD,EAAEC,KAAmD;QAC9Gb;QAASY,WAAwCC;IAA7D,MAAMC,OAAMd,YAAAA,SAASY,0BAAAA,YAAAA,MAAOG,EAAE,qBAATH,SAAW,CAAC,cAAc,aAAnCZ,YAAwCA,SAASa,0BAAAA,YAAAA,MAAOE,EAAE,qBAATF,SAAW,CAAC,cAAc;IACvF,IAAIC,QAAQ,MAAM,OAAOA;IACzB,MAAMC,KAAKF,yBAAAA,MAAOE,EAAE;IACpB,IAAIA,CAAAA,sBAAAA,EAAI,CAAC,WAAW,MAAK,cAAcA,EAAE,CAAC,QAAQ,KAAK,UAAUA,EAAE,CAAC,SAAS,KAAK,QAAQ,OAAOL;IACjG,OAAO;AACT;AAEA,0FAA0F,GAC1F,OAAO,SAASM,oBAAoBC,KAInC;IACC,IAAIA,MAAMC,YAAY,KAAK,GAAG,OAAO;IACrC,IAAInB,OAAOoB,IAAI,CAACF,MAAMG,WAAW,KAAKrB,OAAOoB,IAAI,CAACF,MAAMI,GAAG,GAAG,OAAO;IACrE,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASC,qBACdC,KAAe,EACfC,MAAc,EACdC,QAA8D;IAE9D,MAAMC,MAAMH;IAIZ,MAAMI,QAA+B,EAAE;IACvC,MAAMC,OAAO,IAAIC;IACjB,MAAMC,QAAQ,CAACC,IAAYC,UAAyBd;YA6B9Be;QA5BpB,IAAIL,KAAKM,GAAG,CAACH,KAAK;QAClBH,KAAKO,GAAG,CAACJ;QACT,MAAME,OAAOP,GAAG,CAACK,GAAG;QACpB,IAAI,CAACE,MAAM;QACX,MAAMG,QAAQX,SAASY,GAAG,CAACC,OAAO,CAACP;QACnC,MAAMQ,OAAOH,SAAS,IAAIA,QAAQlB;QAClC,IAAIe,KAAKO,WAAW,KAAK,SAAS;gBACpBP,aACOA;YADnB,MAAMQ,OAAMR,cAAAA,KAAKS,KAAK,qBAAVT,WAAY,CAAC,MAAM;YAC/B,MAAMZ,MAAM,SAAOY,eAAAA,KAAKS,KAAK,qBAAVT,YAAY,CAAC,MAAM,MAAK,WAAW,AAACA,KAAKS,KAAK,CAAC,MAAM,CAAYC,IAAI,KAAK;YAC7F,IAAI,AAAC,CAAA,OAAOF,QAAQ,YAAY,CAACA,IAAIE,IAAI,EAAC,KAAMtB,KAAK;0BAarBI;oBAV5BZ;gBAFF,MAAMA,QAAQmB,WAAWN,GAAG,CAACM,SAAS,GAAGY;gBACzC,MAAMC,iBACJhC,0BAAAA,eAAAA,MAAOU,KAAK,qBAAZV,aAAciC,IAAI,CAAC,CAACC;wBAA0BrB;2BAAhBqB,UAAUhB,MAAML,EAAAA,aAAAA,GAAG,CAACqB,MAAM,qBAAVrB,WAAYc,WAAW,MAAK;qCAAW;gBACvFb,MAAMqB,IAAI,CAAC;oBACTC,SAASlB;oBACTmB,SAASrC,QAAQmB,WAAW;oBAC5Ba;oBACAxB;oBACA8B,QAAQxC,WAAWsB,MAAMpB;oBACzBK,cAAcqB;oBACda,MAAMpC,oBAAoB;wBACxBE,cAAcqB;wBACdnB,aAAamB,QAAQ,KAAKd,uBAAAA,SAAS4B,KAAK,CAACd,KAAK,YAApBd,uBAAwB,KAAM;wBACxDJ;oBACF;gBACF;YACF;QACF;QACA,KAAK,MAAM0B,UAASd,cAAAA,KAAKV,KAAK,YAAVU,cAAc,EAAE,CAAEH,MAAMiB,OAAOhB,IAAIQ;IACzD;IACAT,MAAMN,QAAQ,MAAM,CAAC;IACrB,OAAOG;AACT;AAEA,uEAAuE,GACvE,OAAO,MAAM2B,2BAA2B1D,oBAAmB;AAE3D;;;;;;CAMC,GACD,OAAO,MAAM2D,qBAAqDC,OAAOC,MAAM,CAAC;IAAEhB,KAAK;IAAIiB,OAAO;IAAGC,QAAQ;AAAE,GAAE;AAIjH;;;;CAIC,GACD,MAAMC,gBAA+E;IACnFC,MAAM;QAAC;QAAQ;QAAQ;KAAQ;IAC/BC,OAAO;QAAC;QAAS;KAAO;IACxBC,SAAS;QAAC;QAAQ;QAAU;KAAQ;AACtC;AAEA,MAAMC,eAAeR,OAAOS,IAAI,CAACX;AAEjC,qFAAqF,GACrF,OAAO,SAASY,mBAAmBC,IAAY;IAC7C,IAAIC,OAAO;IACX,IAAK,IAAIhC,QAAQ,GAAGA,QAAQ+B,KAAKE,MAAM,EAAEjC,SAAS,EAAG;QACnDgC,QAAQD,KAAKG,UAAU,CAAClC;QACxBgC,OAAOG,KAAKC,IAAI,CAACJ,MAAM;IACzB;IACA,OAAOA,SAAS;AAClB;AAEA,SAASK,OAAUC,IAAkB,EAAEC,EAAU;IAC/C,IAAI,CAACD,KAAKL,MAAM,EAAE,OAAO,EAAE;IAC3B,MAAMO,QAAQD,KAAKD,KAAKL,MAAM;IAC9B,OAAO;WAAIK,KAAKG,KAAK,CAACD;WAAWF,KAAKG,KAAK,CAAC,GAAGD;KAAO;AACxD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASE,sBACdnD,KAAqC,EACrCwC,IAAY;IAEZ,MAAMY,OAAOb,mBAAmBC;IAChC,MAAMa,QAA+B;QAAC;QAAQ;QAAS;KAAU;IACjE,MAAMC,SAAStD,MACZD,GAAG,CAAC,CAACwD,MAAM9C,QAAW,CAAA;YAAE8C;YAAM9C;QAAM,CAAA,GACpC+C,IAAI,CAAC,CAACC,GAAGC,IAAML,MAAM1C,OAAO,CAAC8C,EAAEF,IAAI,CAAC9B,IAAI,IAAI4B,MAAM1C,OAAO,CAAC+C,EAAEH,IAAI,CAAC9B,IAAI,KAAKgC,EAAEhD,KAAK,GAAGiD,EAAEjD,KAAK;IAC9F,MAAMiB,QAAwC1B,MAAMD,GAAG,CAAC,IAAMkB;IAC9D,yEAAyE,GACzE,MAAM0C,WAAW,IAAIC;IACrBN,OAAOO,OAAO,CAAC,CAAC,EAAEN,IAAI,EAAE9C,KAAK,EAAE,EAAEqD;YAa7BC;QAZF,wEAAwE;QACxE,MAAMC,OAAOT,KAAK9B,IAAI,KAAK,UAAU;eAAIQ,cAAcE,KAAK;SAAC,GAAGW,OAAOb,aAAa,CAACsB,KAAK9B,IAAI,CAAC,EAAE2B;QACjG,MAAMa,OAAOnB,OACXT,aAAa6B,MAAM,CAAC,CAACC,OAAS,CAACH,KAAKI,QAAQ,CAACD,QAC7Cf;QAEF,uEAAuE;QACvE,yDAAyD;QACzD,MAAMiB,aAAa;eAAIL;eAASC;SAAK,CAACC,MAAM,CAAC,CAACC,OAASZ,KAAK9B,IAAI,KAAK,WAAW0C,SAAS;QACzF,MAAMJ,SAASM,WAAWlD,IAAI,CAAC,CAACmD,YAAc,CAACX,SAASpD,GAAG,CAAC+D;QAC5D,MAAMC,SAAS,IAAIrE,IAAI;YAACwB,KAAK,CAACjB,QAAQ,EAAE;YAAEiB,KAAK,CAACjB,QAAQ,EAAE;SAAC,CAACyD,MAAM,CAACM;QACnE,MAAML,QACJJ,OAAAA,iBAAAA,SACA;eAAIM;SAAW,CACZb,IAAI,CAAC,CAACC,GAAGC;gBAAOC,eAA0BA;mBAA3B,EAACA,gBAAAA,SAASc,GAAG,CAAChB,cAAbE,gBAAmB,CAAC,OAAMA,iBAAAA,SAASc,GAAG,CAACf,cAAbC,iBAAmB,CAAC;WAC9DxC,IAAI,CAAC,CAACmD,YAAc,CAACC,OAAOhE,GAAG,CAAC+D,uBAHnCP,OAIAM,UAAU,CAAC,EAAE;QACf3C,KAAK,CAACjB,MAAM,GAAG0D;QACfR,SAASe,GAAG,CAACP,MAAML;IACrB;IACA,OAAOpC,MAAM3B,GAAG,CAAC,CAACoE;QAChB,MAAMQ,QAAQhD,wBAAwB,CAACwC,KAAoB;QAC3D,OAAO;YAAErD,KAAK6D,MAAM7D,GAAG;YAAEiB,OAAO4C,MAAM5C,KAAK;YAAEC,QAAQ2C,MAAM3C,MAAM;YAAEtC,KAAKiF,MAAMjF,GAAG;QAAC;IACpF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASkF,sBACdhF,KAAe,EACfI,KAAqC,EACrC6E,MAAkD;IAElD,MAAMC,OAAO,aAAMlF;IACnB,0EAA0E;IAC1E,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,MAAMmF,OAAO/E,MAAMgF,SAAS,CAAC,CAACC,GAAGxE,QAAU+D,QAAQK,MAAM,CAACpE,MAAM,IAAIqE,IAAI,CAAC9E,KAAK,CAACS,MAAM,CAACa,OAAO,CAAC;IAC9FtB,MAAM6D,OAAO,CAAC,CAACN,MAAM9C;YAIEH;YASbqE;QAZR,MAAMA,QAAQE,MAAM,CAACpE,MAAM;QAC3B,MAAMH,OAAOwE,IAAI,CAACvB,KAAKjC,OAAO,CAAC;QAC/B,IAAI,CAACqD,SAAS,CAACrE,MAAM;QACrB,MAAMS,QAAQ,cAAOT,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAyD,CAAC;QAC/E,OAAOS,KAAK,CAAC,UAAU;QACvB+D,IAAI,CAACvB,KAAKjC,OAAO,CAAC,GAAG,aAChBhB;YACHS,OAAO,aACFA;gBACHD,KAAK6D,MAAM7D,GAAG;gBACdoE,gBAAgBP,MAAM5C,KAAK;gBAC3BoD,iBAAiBR,MAAM3C,MAAM;eACzB2C,EAAAA,aAAAA,MAAMjF,GAAG,qBAATiF,WAAW3D,IAAI,MAAK;gBAAEtB,KAAKiF,MAAMjF,GAAG,CAACsB,IAAI;YAAG,IAAI,CAAC,GACjDP,UAAUsE,QAAQxB,KAAK9B,IAAI,KAAK,SAAS;gBAAE2D,SAAS;YAAQ,IAAI,CAAC;;QAGzE,IAAI7B,KAAKrC,MAAM,IAAIqC,KAAKhC,OAAO,IAAIuD,IAAI,CAACvB,KAAKhC,OAAO,CAAC,EAAE;gBAI1CrC;YAHX,MAAMA,QAAQ4F,IAAI,CAACvB,KAAKhC,OAAO,CAAC;YAChCuD,IAAI,CAACvB,KAAKhC,OAAO,CAAC,GAAG,aAChBrC;gBACHU,OAAO,EAAEV,eAAAA,KAAK,CAAC,QAAQ,YAAdA,eAA2C,EAAE,EAAEgF,MAAM,CAAC,CAAC9C,QAAUA,UAAUmC,KAAKrC,MAAM;;YAEjG,OAAO4D,IAAI,CAACvB,KAAKrC,MAAM,CAAC;QAC1B;IACF;IACA,OAAO4D;AACT;AAcA;;;;;;CAMC,GACD,OAAO,eAAeO,wBACpBzF,KAAe,EACfN,KASC;IAED,IAAI;QACF,MAAMU,QAAQL,qBAAqBC,OAAON,MAAMO,MAAM,EAAE;YACtDa,KAAKpB,MAAMgG,UAAU;YACrB5D,OAAOpC,MAAMiG,YAAY;QAC3B;QACA,IAAI,CAACvF,MAAM0C,MAAM,EAAE,OAAO9C;QAC1B,IAAI4F,QAAoD,EAAE;QAC1D,IAAIlG,MAAMmG,MAAM,EAAE;YAChB,IAAI;gBACFD,QAAQ,MAAMlG,MAAMmG,MAAM,CAACzF;YAC7B,EAAE,OAAO0F,OAAO;gBACdC,QAAQC,IAAI,CAAC,6EAA6E;oBAAEF;gBAAM;gBAClGF,QAAQ,EAAE;YACZ;QACF;QACA,sEAAsE;QACtE,2EAA2E;QAC3E,MAAMK,gBAAgB1H,+BAA+BmB,MAAMwG,QAAQ;QACnE,MAAMC,WAAW5C,sBAAsBnD,OAAOV,MAAMkD,IAAI,EAAEzC,GAAG,CAAC,CAAC4E,QAC7DkB,gBAAgB,aAAKlB;gBAAOjF,KAAKxB,cAAc,qBAAqBoB,MAAMwG,QAAQ;iBAAMnB;QAE1F,OAAOC,sBACLhF,OACAI,OACAA,MAAMD,GAAG,CAAC,CAACiG,OAAOvF;gBAGTsF;YAFP,MAAME,SAAST,KAAK,CAAC/E,MAAM;YAC3B,IAAIwF,QAAQ,OAAOA,OAAOnF,GAAG,GAAGmF,SAAS;YACzC,QAAOF,kBAAAA,QAAQ,CAACtF,MAAM,YAAfsF,kBAAmB;QAC5B;IAEJ,EAAE,OAAOL,OAAO;QACdC,QAAQD,KAAK,CAAC,8DAA8D;YAAEA;QAAM;QACpF,OAAO9F;IACT;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { AiEmailType } from '../jobs/ai-job-email-step';
|
|
18
|
+
import type { AiJobKind } from './ai-jobs.types';
|
|
19
|
+
import type { AiPageType } from './ai-page-job';
|
|
20
|
+
import { type AiTemplateSubject } from './ai-template-subjects';
|
|
21
|
+
/**
|
|
22
|
+
* WHAT EVERY "CREATE WITH AI" BRIEF SAYS AND SENDS (AGL-2907, AGL-3043,
|
|
23
|
+
* AGL-3051, AGL-3596): each kind's words, the choices beside the brief, and
|
|
24
|
+
* the job and inputs they start. Pure, so the console's brief dialog and the
|
|
25
|
+
* door the native apps read (AGL-3854) say the same thing from one place.
|
|
26
|
+
*/
|
|
27
|
+
/** The longest brief a job admits. */
|
|
28
|
+
export declare const AI_BRIEF_MAX_CHARS = 4000;
|
|
29
|
+
/** The job kinds a member describes from a console page. */
|
|
30
|
+
export type AiBriefKind = Extract<AiJobKind, 'page' | 'template' | 'layout' | 'form' | 'component' | 'email' | 'campaign'>;
|
|
31
|
+
/** What the dialog says for one kind. */
|
|
32
|
+
export interface AiBriefCopy {
|
|
33
|
+
title: string;
|
|
34
|
+
/** The brief box's label. */
|
|
35
|
+
briefLabel: string;
|
|
36
|
+
placeholder: string;
|
|
37
|
+
/** What happens after the member starts the job, under the fields. */
|
|
38
|
+
next: string;
|
|
39
|
+
/** The action that starts the job. */
|
|
40
|
+
submit: string;
|
|
41
|
+
/** What the dialog says once the job exists, above its live status. */
|
|
42
|
+
started: string;
|
|
43
|
+
/** What it says when the door gave no reason, or could not be reached. */
|
|
44
|
+
failed: string;
|
|
45
|
+
}
|
|
46
|
+
export declare const AI_BRIEF_COPY: Readonly<Record<AiBriefKind, AiBriefCopy>>;
|
|
47
|
+
/**
|
|
48
|
+
* The kinds of email a member picks from, as their chips name them. The jobs'
|
|
49
|
+
* own catalog (`AI_EMAIL_TYPES`) also holds `reply`, which only a job the
|
|
50
|
+
* platform composes sets, so it is not offered here.
|
|
51
|
+
*/
|
|
52
|
+
export declare const AI_EMAIL_TYPE_LABELS: Readonly<Record<Exclude<AiEmailType, 'reply'>, string>>;
|
|
53
|
+
/** Each template subject as its chip names it: what one page is drawn for. */
|
|
54
|
+
export declare const AI_TEMPLATE_SUBJECT_LABELS: Readonly<Record<AiTemplateSubject, string>>;
|
|
55
|
+
/** What a member picked beside the brief; each kind reads only its own. */
|
|
56
|
+
export interface AiBriefChoice {
|
|
57
|
+
/** A page's type, or `null` for none. */
|
|
58
|
+
pageType: AiPageType | null;
|
|
59
|
+
/** What a template's pages are drawn for, or `null` until one is picked. */
|
|
60
|
+
subject: AiTemplateSubject | null;
|
|
61
|
+
/** A collection entry template's collection, or `null` until one is picked. */
|
|
62
|
+
collectionId: string | null;
|
|
63
|
+
/** An email's or a campaign's kind of email; `null` or absent for none. */
|
|
64
|
+
emailType?: Exclude<AiEmailType, 'reply'> | null;
|
|
65
|
+
/** An email's door only: also draft the campaign that would send it. */
|
|
66
|
+
withCampaign?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export declare const AI_BRIEF_NO_CHOICE: AiBriefChoice;
|
|
69
|
+
/**
|
|
70
|
+
* The job the door starts: its own kind, except an email that also asked for
|
|
71
|
+
* its campaign, which is a `campaign` job — the campaign step writes the same
|
|
72
|
+
* design and drafts the campaign around it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function aiBriefJobKind(kind: AiBriefKind, choice: AiBriefChoice): AiBriefKind;
|
|
75
|
+
/**
|
|
76
|
+
* The job's `inputs` for what the member picked, or `null` while the kind's
|
|
77
|
+
* door would refuse them.
|
|
78
|
+
*
|
|
79
|
+
* A template's are read by the parser its door reads, so the dialog cannot
|
|
80
|
+
* send a subject the door does not know or an entry page with no collection.
|
|
81
|
+
* A page's type is optional, and so is an email's or a campaign's kind of
|
|
82
|
+
* email (`inputs.emailType`, which both steps read). A layout, a form and a component read no inputs
|
|
83
|
+
* at all (AGL-2909, AGL-2913, AGL-2908), so theirs are empty.
|
|
84
|
+
*/
|
|
85
|
+
export declare function aiBriefJobInputs(kind: AiBriefKind, choice: AiBriefChoice): Record<string, string> | null;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { parseAiTemplateJobInputs } from "./ai-template-subjects.js";
|
|
17
|
+
/**
|
|
18
|
+
* WHAT EVERY "CREATE WITH AI" BRIEF SAYS AND SENDS (AGL-2907, AGL-3043,
|
|
19
|
+
* AGL-3051, AGL-3596): each kind's words, the choices beside the brief, and
|
|
20
|
+
* the job and inputs they start. Pure, so the console's brief dialog and the
|
|
21
|
+
* door the native apps read (AGL-3854) say the same thing from one place.
|
|
22
|
+
*/ /** The longest brief a job admits. */ export const AI_BRIEF_MAX_CHARS = 4000;
|
|
23
|
+
export const AI_BRIEF_COPY = {
|
|
24
|
+
page: {
|
|
25
|
+
title: 'Describe a page',
|
|
26
|
+
briefLabel: 'What is the page for?',
|
|
27
|
+
placeholder: 'A landing page for our spring roof inspection offer, with the quote request form',
|
|
28
|
+
next: 'A plan comes first, and nothing is built until you confirm it. The page uses your ' + 'theme and what the site already has, and the plan lists anything the job builds first.',
|
|
29
|
+
submit: 'Plan the page',
|
|
30
|
+
started: 'The page is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The page is built as an unpublished draft.',
|
|
31
|
+
failed: 'The page could not be started. Try again.'
|
|
32
|
+
},
|
|
33
|
+
template: {
|
|
34
|
+
title: 'Describe a page template',
|
|
35
|
+
briefLabel: 'What should each page show?',
|
|
36
|
+
placeholder: 'A page for each of our team members: their name and photo, their role, a short ' + 'biography and a way to get in touch',
|
|
37
|
+
next: 'A plan comes first, and nothing is built until you confirm it. What changes from one ' + 'page to the next, such as a name or a photo, is filled in from each record rather ' + 'than typed.',
|
|
38
|
+
submit: 'Plan the template',
|
|
39
|
+
started: 'The template is being planned. Review the plan here or in AI jobs, and confirm it ' + 'to build. The template is built as a draft in your library, and nothing on your ' + 'site uses it until you do.',
|
|
40
|
+
failed: 'The template could not be started. Try again.'
|
|
41
|
+
},
|
|
42
|
+
layout: {
|
|
43
|
+
title: 'Describe a layout',
|
|
44
|
+
briefLabel: 'What should the layout hold?',
|
|
45
|
+
placeholder: 'A header with our name and links to Services, About and Contact, and a footer with ' + 'our address and opening hours',
|
|
46
|
+
next: 'A plan comes first, and nothing is built until you confirm it. The layout links your ' + 'pages, and places your navigation or menu component when the site has one.',
|
|
47
|
+
submit: 'Plan the layout',
|
|
48
|
+
started: 'The layout is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The layout is built as a draft, and no page uses it until you assign it.',
|
|
49
|
+
failed: 'The layout could not be started. Try again.'
|
|
50
|
+
},
|
|
51
|
+
form: {
|
|
52
|
+
title: 'Describe a form',
|
|
53
|
+
briefLabel: 'What is the form for?',
|
|
54
|
+
placeholder: 'A quote request form for a roofing company, asking what needs fixing and the best ' + 'time to call',
|
|
55
|
+
next: 'A plan comes first, and nothing is built until you confirm it. The form’s fields, its ' + 'marketing consent and where each submission goes are planned together.',
|
|
56
|
+
submit: 'Plan the form',
|
|
57
|
+
started: 'The form is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The form is built as a draft, and it collects nothing until you place it on ' + 'a page.',
|
|
58
|
+
failed: 'The form could not be started. Try again.'
|
|
59
|
+
},
|
|
60
|
+
component: {
|
|
61
|
+
title: 'Describe a reusable component',
|
|
62
|
+
briefLabel: 'What should the component show?',
|
|
63
|
+
placeholder: 'A testimonial card with the customer’s name, their role, their quote and a photo',
|
|
64
|
+
next: 'A plan comes first, and nothing is built until you confirm it. What each page can ' + 'change, such as a name or a photo, becomes a property of the component.',
|
|
65
|
+
submit: 'Plan the component',
|
|
66
|
+
started: 'The component is being planned. Review the plan here or in AI jobs, and confirm it ' + 'to build. The component is built as a draft, and no page shows it until you insert ' + 'it.',
|
|
67
|
+
failed: 'The component could not be started. Try again.'
|
|
68
|
+
},
|
|
69
|
+
email: {
|
|
70
|
+
title: 'Describe an email',
|
|
71
|
+
briefLabel: 'What is the email for?',
|
|
72
|
+
placeholder: 'Announce the autumn menu to our regulars, and point them at the class page to book ' + 'the Tuesday bread class',
|
|
73
|
+
next: 'A plan comes first, and nothing is built until you confirm it. The email is built ' + 'from email blocks, with three subject lines and three preheaders to choose from. ' + 'Nothing is sent or scheduled.',
|
|
74
|
+
submit: 'Plan the email',
|
|
75
|
+
started: 'The email is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The email is saved as a draft template, and nothing sends it until a ' + 'campaign does.',
|
|
76
|
+
failed: 'The email could not be started. Try again.'
|
|
77
|
+
},
|
|
78
|
+
campaign: {
|
|
79
|
+
title: 'Describe a campaign',
|
|
80
|
+
briefLabel: 'What is the campaign’s email for?',
|
|
81
|
+
placeholder: 'Announce the autumn menu to our regulars, and point them at the class page to book ' + 'the Tuesday bread class',
|
|
82
|
+
next: 'A plan comes first, and nothing is built until you confirm it. The job writes the ' + 'email and a draft campaign that would send it. The campaign is aimed at nobody and ' + 'sends nothing until you pick who receives it and schedule it.',
|
|
83
|
+
submit: 'Plan the campaign',
|
|
84
|
+
started: 'The campaign is being planned. Review the plan here or in AI jobs, and confirm it ' + 'to build. The email and the campaign are saved as drafts, and nothing is sent.',
|
|
85
|
+
failed: 'The campaign could not be started. Try again.'
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The kinds of email a member picks from, as their chips name them. The jobs'
|
|
90
|
+
* own catalog (`AI_EMAIL_TYPES`) also holds `reply`, which only a job the
|
|
91
|
+
* platform composes sets, so it is not offered here.
|
|
92
|
+
*/ export const AI_EMAIL_TYPE_LABELS = {
|
|
93
|
+
welcome: 'Welcome',
|
|
94
|
+
newsletter: 'Newsletter',
|
|
95
|
+
launch: 'Launch',
|
|
96
|
+
abandonedCart: 'Cart reminder',
|
|
97
|
+
eventReminder: 'Event reminder'
|
|
98
|
+
};
|
|
99
|
+
/** Each template subject as its chip names it: what one page is drawn for. */ export const AI_TEMPLATE_SUBJECT_LABELS = {
|
|
100
|
+
entry: 'Collection entry',
|
|
101
|
+
product: 'Product',
|
|
102
|
+
author: 'Author'
|
|
103
|
+
};
|
|
104
|
+
export const AI_BRIEF_NO_CHOICE = {
|
|
105
|
+
pageType: null,
|
|
106
|
+
subject: null,
|
|
107
|
+
collectionId: null,
|
|
108
|
+
emailType: null,
|
|
109
|
+
withCampaign: false
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The job the door starts: its own kind, except an email that also asked for
|
|
113
|
+
* its campaign, which is a `campaign` job — the campaign step writes the same
|
|
114
|
+
* design and drafts the campaign around it.
|
|
115
|
+
*/ export function aiBriefJobKind(kind, choice) {
|
|
116
|
+
return kind === 'email' && choice.withCampaign ? 'campaign' : kind;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The job's `inputs` for what the member picked, or `null` while the kind's
|
|
120
|
+
* door would refuse them.
|
|
121
|
+
*
|
|
122
|
+
* A template's are read by the parser its door reads, so the dialog cannot
|
|
123
|
+
* send a subject the door does not know or an entry page with no collection.
|
|
124
|
+
* A page's type is optional, and so is an email's or a campaign's kind of
|
|
125
|
+
* email (`inputs.emailType`, which both steps read). A layout, a form and a component read no inputs
|
|
126
|
+
* at all (AGL-2909, AGL-2913, AGL-2908), so theirs are empty.
|
|
127
|
+
*/ export function aiBriefJobInputs(kind, choice) {
|
|
128
|
+
if (kind === 'page') return choice.pageType ? {
|
|
129
|
+
pageType: choice.pageType
|
|
130
|
+
} : {};
|
|
131
|
+
if (kind === 'email' || kind === 'campaign') {
|
|
132
|
+
return choice.emailType ? {
|
|
133
|
+
emailType: choice.emailType
|
|
134
|
+
} : {};
|
|
135
|
+
}
|
|
136
|
+
if (kind === 'template') {
|
|
137
|
+
const inputs = parseAiTemplateJobInputs({
|
|
138
|
+
subject: choice.subject,
|
|
139
|
+
collectionId: choice.collectionId
|
|
140
|
+
});
|
|
141
|
+
if (typeof inputs === 'string') return null;
|
|
142
|
+
return inputs.collectionId ? {
|
|
143
|
+
subject: inputs.subject,
|
|
144
|
+
collectionId: inputs.collectionId
|
|
145
|
+
} : {
|
|
146
|
+
subject: inputs.subject
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=ai-brief-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-brief-copy.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 { AiEmailType } from '../jobs/ai-job-email-step'\nimport type { AiJobKind } from './ai-jobs.types'\nimport type { AiPageType } from './ai-page-job'\nimport { parseAiTemplateJobInputs, type AiTemplateSubject } from './ai-template-subjects'\n\n/**\n * WHAT EVERY \"CREATE WITH AI\" BRIEF SAYS AND SENDS (AGL-2907, AGL-3043,\n * AGL-3051, AGL-3596): each kind's words, the choices beside the brief, and\n * the job and inputs they start. Pure, so the console's brief dialog and the\n * door the native apps read (AGL-3854) say the same thing from one place.\n */\n\n/** The longest brief a job admits. */\nexport const AI_BRIEF_MAX_CHARS = 4_000\n\n/** The job kinds a member describes from a console page. */\nexport type AiBriefKind = Extract<\n AiJobKind,\n 'page' | 'template' | 'layout' | 'form' | 'component' | 'email' | 'campaign'\n>\n\n/** What the dialog says for one kind. */\nexport interface AiBriefCopy {\n title: string\n /** The brief box's label. */\n briefLabel: string\n placeholder: string\n /** What happens after the member starts the job, under the fields. */\n next: string\n /** The action that starts the job. */\n submit: string\n /** What the dialog says once the job exists, above its live status. */\n started: string\n /** What it says when the door gave no reason, or could not be reached. */\n failed: string\n}\n\nexport const AI_BRIEF_COPY: Readonly<Record<AiBriefKind, AiBriefCopy>> = {\n page: {\n title: 'Describe a page',\n briefLabel: 'What is the page for?',\n placeholder:\n 'A landing page for our spring roof inspection offer, with the quote request form',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The page uses your ' +\n 'theme and what the site already has, and the plan lists anything the job builds first.',\n submit: 'Plan the page',\n started:\n 'The page is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The page is built as an unpublished draft.',\n failed: 'The page could not be started. Try again.',\n },\n template: {\n title: 'Describe a page template',\n briefLabel: 'What should each page show?',\n placeholder:\n 'A page for each of our team members: their name and photo, their role, a short ' +\n 'biography and a way to get in touch',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What changes from one ' +\n 'page to the next, such as a name or a photo, is filled in from each record rather ' +\n 'than typed.',\n submit: 'Plan the template',\n started:\n 'The template is being planned. Review the plan here or in AI jobs, and confirm it ' +\n 'to build. The template is built as a draft in your library, and nothing on your ' +\n 'site uses it until you do.',\n failed: 'The template could not be started. Try again.',\n },\n layout: {\n title: 'Describe a layout',\n briefLabel: 'What should the layout hold?',\n placeholder:\n 'A header with our name and links to Services, About and Contact, and a footer with ' +\n 'our address and opening hours',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The layout links your ' +\n 'pages, and places your navigation or menu component when the site has one.',\n submit: 'Plan the layout',\n started:\n 'The layout is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The layout is built as a draft, and no page uses it until you assign it.',\n failed: 'The layout could not be started. Try again.',\n },\n form: {\n title: 'Describe a form',\n briefLabel: 'What is the form for?',\n placeholder:\n 'A quote request form for a roofing company, asking what needs fixing and the best ' +\n 'time to call',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The form’s fields, its ' +\n 'marketing consent and where each submission goes are planned together.',\n submit: 'Plan the form',\n started:\n 'The form is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The form is built as a draft, and it collects nothing until you place it on ' +\n 'a page.',\n failed: 'The form could not be started. Try again.',\n },\n component: {\n title: 'Describe a reusable component',\n briefLabel: 'What should the component show?',\n placeholder:\n 'A testimonial card with the customer’s name, their role, their quote and a photo',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What each page can ' +\n 'change, such as a name or a photo, becomes a property of the component.',\n submit: 'Plan the component',\n started:\n 'The component is being planned. Review the plan here or in AI jobs, and confirm it ' +\n 'to build. The component is built as a draft, and no page shows it until you insert ' +\n 'it.',\n failed: 'The component could not be started. Try again.',\n },\n email: {\n title: 'Describe an email',\n briefLabel: 'What is the email for?',\n placeholder:\n 'Announce the autumn menu to our regulars, and point them at the class page to book ' +\n 'the Tuesday bread class',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The email is built ' +\n 'from email blocks, with three subject lines and three preheaders to choose from. ' +\n 'Nothing is sent or scheduled.',\n submit: 'Plan the email',\n started:\n 'The email is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The email is saved as a draft template, and nothing sends it until a ' +\n 'campaign does.',\n failed: 'The email could not be started. Try again.',\n },\n campaign: {\n title: 'Describe a campaign',\n briefLabel: 'What is the campaign’s email for?',\n placeholder:\n 'Announce the autumn menu to our regulars, and point them at the class page to book ' +\n 'the Tuesday bread class',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The job writes the ' +\n 'email and a draft campaign that would send it. The campaign is aimed at nobody and ' +\n 'sends nothing until you pick who receives it and schedule it.',\n submit: 'Plan the campaign',\n started:\n 'The campaign is being planned. Review the plan here or in AI jobs, and confirm it ' +\n 'to build. The email and the campaign are saved as drafts, and nothing is sent.',\n failed: 'The campaign could not be started. Try again.',\n },\n}\n\n/**\n * The kinds of email a member picks from, as their chips name them. The jobs'\n * own catalog (`AI_EMAIL_TYPES`) also holds `reply`, which only a job the\n * platform composes sets, so it is not offered here.\n */\nexport const AI_EMAIL_TYPE_LABELS: Readonly<Record<Exclude<AiEmailType, 'reply'>, string>> = {\n welcome: 'Welcome',\n newsletter: 'Newsletter',\n launch: 'Launch',\n abandonedCart: 'Cart reminder',\n eventReminder: 'Event reminder',\n}\n\n/** Each template subject as its chip names it: what one page is drawn for. */\nexport const AI_TEMPLATE_SUBJECT_LABELS: Readonly<Record<AiTemplateSubject, string>> = {\n entry: 'Collection entry',\n product: 'Product',\n author: 'Author',\n}\n\n/** What a member picked beside the brief; each kind reads only its own. */\nexport interface AiBriefChoice {\n /** A page's type, or `null` for none. */\n pageType: AiPageType | null\n /** What a template's pages are drawn for, or `null` until one is picked. */\n subject: AiTemplateSubject | null\n /** A collection entry template's collection, or `null` until one is picked. */\n collectionId: string | null\n /** An email's or a campaign's kind of email; `null` or absent for none. */\n emailType?: Exclude<AiEmailType, 'reply'> | null\n /** An email's door only: also draft the campaign that would send it. */\n withCampaign?: boolean\n}\n\nexport const AI_BRIEF_NO_CHOICE: AiBriefChoice = {\n pageType: null,\n subject: null,\n collectionId: null,\n emailType: null,\n withCampaign: false,\n}\n\n/**\n * The job the door starts: its own kind, except an email that also asked for\n * its campaign, which is a `campaign` job — the campaign step writes the same\n * design and drafts the campaign around it.\n */\nexport function aiBriefJobKind(kind: AiBriefKind, choice: AiBriefChoice): AiBriefKind {\n return kind === 'email' && choice.withCampaign ? 'campaign' : kind\n}\n\n/**\n * The job's `inputs` for what the member picked, or `null` while the kind's\n * door would refuse them.\n *\n * A template's are read by the parser its door reads, so the dialog cannot\n * send a subject the door does not know or an entry page with no collection.\n * A page's type is optional, and so is an email's or a campaign's kind of\n * email (`inputs.emailType`, which both steps read). A layout, a form and a component read no inputs\n * at all (AGL-2909, AGL-2913, AGL-2908), so theirs are empty.\n */\nexport function aiBriefJobInputs(\n kind: AiBriefKind,\n choice: AiBriefChoice,\n): Record<string, string> | null {\n if (kind === 'page') return choice.pageType ? { pageType: choice.pageType } : {}\n if (kind === 'email' || kind === 'campaign') {\n return choice.emailType ? { emailType: choice.emailType } : {}\n }\n if (kind === 'template') {\n const inputs = parseAiTemplateJobInputs({\n subject: choice.subject,\n collectionId: choice.collectionId,\n })\n if (typeof inputs === 'string') return null\n return inputs.collectionId\n ? { subject: inputs.subject, collectionId: inputs.collectionId }\n : { subject: inputs.subject }\n }\n return {}\n}\n"],"names":["parseAiTemplateJobInputs","AI_BRIEF_MAX_CHARS","AI_BRIEF_COPY","page","title","briefLabel","placeholder","next","submit","started","failed","template","layout","form","component","email","campaign","AI_EMAIL_TYPE_LABELS","welcome","newsletter","launch","abandonedCart","eventReminder","AI_TEMPLATE_SUBJECT_LABELS","entry","product","author","AI_BRIEF_NO_CHOICE","pageType","subject","collectionId","emailType","withCampaign","aiBriefJobKind","kind","choice","aiBriefJobInputs","inputs"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD,SAASA,wBAAwB,QAAgC,4BAAwB;AAEzF;;;;;CAKC,GAED,oCAAoC,GACpC,OAAO,MAAMC,qBAAqB,KAAK;AAwBvC,OAAO,MAAMC,gBAA4D;IACvEC,MAAM;QACJC,OAAO;QACPC,YAAY;QACZC,aACE;QACFC,MACE,uFACA;QACFC,QAAQ;QACRC,SACE,sFACA;QACFC,QAAQ;IACV;IACAC,UAAU;QACRP,OAAO;QACPC,YAAY;QACZC,aACE,oFACA;QACFC,MACE,0FACA,uFACA;QACFC,QAAQ;QACRC,SACE,uFACA,qFACA;QACFC,QAAQ;IACV;IACAE,QAAQ;QACNR,OAAO;QACPC,YAAY;QACZC,aACE,wFACA;QACFC,MACE,0FACA;QACFC,QAAQ;QACRC,SACE,wFACA;QACFC,QAAQ;IACV;IACAG,MAAM;QACJT,OAAO;QACPC,YAAY;QACZC,aACE,uFACA;QACFC,MACE,2FACA;QACFC,QAAQ;QACRC,SACE,sFACA,wFACA;QACFC,QAAQ;IACV;IACAI,WAAW;QACTV,OAAO;QACPC,YAAY;QACZC,aACE;QACFC,MACE,uFACA;QACFC,QAAQ;QACRC,SACE,wFACA,wFACA;QACFC,QAAQ;IACV;IACAK,OAAO;QACLX,OAAO;QACPC,YAAY;QACZC,aACE,wFACA;QACFC,MACE,uFACA,sFACA;QACFC,QAAQ;QACRC,SACE,uFACA,iFACA;QACFC,QAAQ;IACV;IACAM,UAAU;QACRZ,OAAO;QACPC,YAAY;QACZC,aACE,wFACA;QACFC,MACE,uFACA,wFACA;QACFC,QAAQ;QACRC,SACE,uFACA;QACFC,QAAQ;IACV;AACF,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAMO,uBAAgF;IAC3FC,SAAS;IACTC,YAAY;IACZC,QAAQ;IACRC,eAAe;IACfC,eAAe;AACjB,EAAC;AAED,4EAA4E,GAC5E,OAAO,MAAMC,6BAA0E;IACrFC,OAAO;IACPC,SAAS;IACTC,QAAQ;AACV,EAAC;AAgBD,OAAO,MAAMC,qBAAoC;IAC/CC,UAAU;IACVC,SAAS;IACTC,cAAc;IACdC,WAAW;IACXC,cAAc;AAChB,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASC,eAAeC,IAAiB,EAAEC,MAAqB;IACrE,OAAOD,SAAS,WAAWC,OAAOH,YAAY,GAAG,aAAaE;AAChE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASE,iBACdF,IAAiB,EACjBC,MAAqB;IAErB,IAAID,SAAS,QAAQ,OAAOC,OAAOP,QAAQ,GAAG;QAAEA,UAAUO,OAAOP,QAAQ;IAAC,IAAI,CAAC;IAC/E,IAAIM,SAAS,WAAWA,SAAS,YAAY;QAC3C,OAAOC,OAAOJ,SAAS,GAAG;YAAEA,WAAWI,OAAOJ,SAAS;QAAC,IAAI,CAAC;IAC/D;IACA,IAAIG,SAAS,YAAY;QACvB,MAAMG,SAASrC,yBAAyB;YACtC6B,SAASM,OAAON,OAAO;YACvBC,cAAcK,OAAOL,YAAY;QACnC;QACA,IAAI,OAAOO,WAAW,UAAU,OAAO;QACvC,OAAOA,OAAOP,YAAY,GACtB;YAAED,SAASQ,OAAOR,OAAO;YAAEC,cAAcO,OAAOP,YAAY;QAAC,IAC7D;YAAED,SAASQ,OAAOR,OAAO;QAAC;IAChC;IACA,OAAO,CAAC;AACV"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type AiBriefCopy } from './ai-brief-copy';
|
|
18
|
+
import { AI_JOB_FOLLOW_PHASE_LINES, type AiFollowTone, type AiJobPhase } from './ai-job-activity';
|
|
19
|
+
import { type AiJobOutput, type AiJobSummary } from './ai-jobs.types';
|
|
20
|
+
import { type AiFreeCreditsLeft } from './ai-site-job';
|
|
21
|
+
import type { AiTemplateCollectionOption } from './ai-template-collections';
|
|
22
|
+
/**
|
|
23
|
+
* "CREATE WITH AI" AS THE NATIVE APPS DRAW IT (AGL-3854): the door a list's
|
|
24
|
+
* Create with AI button reads, and the follow view of the job it starts.
|
|
25
|
+
*
|
|
26
|
+
* The console decides each entry from gates its shell already resolved — the
|
|
27
|
+
* plan and the add-on it could buy, `ai.generate`, the release flag and the
|
|
28
|
+
* site's AI switch — and draws the brief and the job from the plugin's own
|
|
29
|
+
* components. An app has neither the shell nor the components, so the AI
|
|
30
|
+
* plugin's `/api/ai/create` door answers both as data: whether the entry is
|
|
31
|
+
* drawn and as what, every word the brief dialog says, and the job as the
|
|
32
|
+
* dialog's follow card reads it, in sentences built here from the same models
|
|
33
|
+
* the console's components read. Nothing here decides what a request may do:
|
|
34
|
+
* the jobs route's own ladder still decides each start, confirm and cancel.
|
|
35
|
+
*/
|
|
36
|
+
/** The lists the apps offer Create with AI on, beside their own create action. */
|
|
37
|
+
export declare const AI_CREATE_KINDS: readonly ["page", "template", "layout", "form", "component"];
|
|
38
|
+
export type AiCreateKind = (typeof AI_CREATE_KINDS)[number];
|
|
39
|
+
export declare function isAiCreateKind(value: unknown): value is AiCreateKind;
|
|
40
|
+
/** One row of chips beside the brief, and the input the pick is sent as. */
|
|
41
|
+
export interface AiCreateChoiceView {
|
|
42
|
+
/** The `inputs` key the pick is sent under. */
|
|
43
|
+
key: 'pageType' | 'subject';
|
|
44
|
+
label: string;
|
|
45
|
+
/** A required pick keeps the start disabled until one is made; an optional one clears when pressed again. */
|
|
46
|
+
required: boolean;
|
|
47
|
+
options: Array<{
|
|
48
|
+
id: string;
|
|
49
|
+
label: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/** A template's collection field, shown once "Collection entry" is picked. */
|
|
53
|
+
export interface AiCreateCollectionsView {
|
|
54
|
+
/** The subject the field belongs to. */
|
|
55
|
+
when: 'entry';
|
|
56
|
+
label: string;
|
|
57
|
+
options: AiTemplateCollectionOption[];
|
|
58
|
+
helper: string;
|
|
59
|
+
}
|
|
60
|
+
/** What is left of the month's AI credits, said before a brief is sent. */
|
|
61
|
+
export interface AiCreateCreditsView {
|
|
62
|
+
text: string;
|
|
63
|
+
/** Nothing is left and the workspace stops at its band, so a start is refused until credits return. */
|
|
64
|
+
exhausted: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** The add-on's offer, on a plan that could buy it and has not (AGL-3601). */
|
|
67
|
+
export interface AiCreateUpsellView {
|
|
68
|
+
title: string;
|
|
69
|
+
does: string;
|
|
70
|
+
offer: string;
|
|
71
|
+
/** The reader may add the add-on (`billing.manage`); the action is drawn only then. */
|
|
72
|
+
canManageBilling: boolean;
|
|
73
|
+
/** "Add Aglyn AI", or `null` for a reader who cannot. */
|
|
74
|
+
action: string | null;
|
|
75
|
+
dismiss: string;
|
|
76
|
+
}
|
|
77
|
+
export interface AiCreateDoorView {
|
|
78
|
+
kind: AiCreateKind;
|
|
79
|
+
/** `create`: the brief; `upsell`: the add-on's offer; `hidden`: the entry is not drawn. */
|
|
80
|
+
door: 'create' | 'upsell' | 'hidden';
|
|
81
|
+
/** The entry's label, "Create with AI". */
|
|
82
|
+
label: string;
|
|
83
|
+
copy: AiBriefCopy | null;
|
|
84
|
+
briefMaxChars: number;
|
|
85
|
+
choices: AiCreateChoiceView[];
|
|
86
|
+
collections: AiCreateCollectionsView | null;
|
|
87
|
+
credits: AiCreateCreditsView | null;
|
|
88
|
+
/** Another page already being built on this site (AGL-3765): a warning, not a stop. */
|
|
89
|
+
building: string | null;
|
|
90
|
+
upsell: AiCreateUpsellView | null;
|
|
91
|
+
}
|
|
92
|
+
/** What the door knows about the month's credits. */
|
|
93
|
+
export type AiCreateCreditsFacts = {
|
|
94
|
+
kind: 'free';
|
|
95
|
+
left: AiFreeCreditsLeft;
|
|
96
|
+
} | {
|
|
97
|
+
kind: 'band';
|
|
98
|
+
used: number;
|
|
99
|
+
limit: number;
|
|
100
|
+
remaining: number;
|
|
101
|
+
stopsAtBand: boolean;
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'unlimited';
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'none';
|
|
106
|
+
};
|
|
107
|
+
/** The credits line above the brief, or `null` where the workspace has no band to read. */
|
|
108
|
+
export declare function aiCreateCreditsView(facts: AiCreateCreditsFacts): AiCreateCreditsView | null;
|
|
109
|
+
/** The chips each kind offers beside its brief, as the console's dialog draws them. */
|
|
110
|
+
export declare function aiCreateChoices(kind: AiCreateKind): AiCreateChoiceView[];
|
|
111
|
+
export interface AiCreateDoorInput {
|
|
112
|
+
kind: AiCreateKind;
|
|
113
|
+
/** The gates that draw no entry at all: `ai.generate`, the release flag, the site's AI switch. */
|
|
114
|
+
shown: boolean;
|
|
115
|
+
/** The plan carries `aiGenerative`. */
|
|
116
|
+
entitled: boolean;
|
|
117
|
+
/** The workspace could buy the add-on that grants it, and the add-on store is open to it. */
|
|
118
|
+
purchasable: boolean;
|
|
119
|
+
canManageBilling: boolean;
|
|
120
|
+
credits: AiCreateCreditsFacts;
|
|
121
|
+
/** The site's content collections, read only for a template's door. */
|
|
122
|
+
collections?: {
|
|
123
|
+
status: 'success' | 'error';
|
|
124
|
+
options: AiTemplateCollectionOption[];
|
|
125
|
+
} | null;
|
|
126
|
+
/** Another page build in flight on the site, for a page's door. */
|
|
127
|
+
buildInFlight?: boolean;
|
|
128
|
+
addonName: string;
|
|
129
|
+
}
|
|
130
|
+
/** The door one list's Create with AI reads. */
|
|
131
|
+
export declare function aiCreateDoorView(input: AiCreateDoorInput): AiCreateDoorView;
|
|
132
|
+
export { AI_JOB_FOLLOW_PHASE_LINES, type AiFollowTone };
|
|
133
|
+
/**
|
|
134
|
+
* Where one output opens in an app, as the console addresses the apps' link
|
|
135
|
+
* resolver answers natively: a page opens its page (the console's preview
|
|
136
|
+
* address, which the apps' Pages claims), a component, layout or template its
|
|
137
|
+
* details, and anything else the page its owner publishes through the
|
|
138
|
+
* record-route seam (a form's own page). `besigner` is the version the job
|
|
139
|
+
* wrote, opened in the Besigner. `null` where the output has no page of its own,
|
|
140
|
+
* names no site, or its owner is not loaded.
|
|
141
|
+
*/
|
|
142
|
+
export declare function aiJobOutputAppLinks(output: Pick<AiJobOutput, 'resource' | 'id' | 'versionId' | 'hostSubdomain'>, orgSlug: string): {
|
|
143
|
+
detail: string;
|
|
144
|
+
besigner: string | null;
|
|
145
|
+
} | null;
|
|
146
|
+
/** The one thing a finished job leads with, as the console's `aiJobPrimaryLink` picks it. */
|
|
147
|
+
export interface AiJobFollowOpen {
|
|
148
|
+
label: string;
|
|
149
|
+
detail: string;
|
|
150
|
+
besigner: string | null;
|
|
151
|
+
}
|
|
152
|
+
export declare function aiJobFollowOpen(job: Pick<AiJobSummary, 'status' | 'outputs'>, orgSlug: string): AiJobFollowOpen | null;
|
|
153
|
+
export interface AiJobFollowView {
|
|
154
|
+
id: string;
|
|
155
|
+
kind: string;
|
|
156
|
+
status: string;
|
|
157
|
+
hostId: string | null;
|
|
158
|
+
phase: AiJobPhase;
|
|
159
|
+
phaseLabel: string;
|
|
160
|
+
line: {
|
|
161
|
+
tone: AiFollowTone;
|
|
162
|
+
text: string;
|
|
163
|
+
};
|
|
164
|
+
/** Still moving, so the apps read it again shortly. */
|
|
165
|
+
moving: boolean;
|
|
166
|
+
/** Finished, failed or canceled: nothing more happens to it. */
|
|
167
|
+
settled: boolean;
|
|
168
|
+
/** "2 of 3 steps", for a job of more than one step. */
|
|
169
|
+
steps: string | null;
|
|
170
|
+
plan: {
|
|
171
|
+
heading: string;
|
|
172
|
+
lines: string[];
|
|
173
|
+
} | null;
|
|
174
|
+
/** The building rules a refused answer broke, as a member reads them. */
|
|
175
|
+
findings: string[];
|
|
176
|
+
/** The checks' own sentence on a refused answer, for staff. */
|
|
177
|
+
detail: string | null;
|
|
178
|
+
/** The plan's estimated cost, read before it is confirmed. */
|
|
179
|
+
estimate: string | null;
|
|
180
|
+
retryRefusal: string | null;
|
|
181
|
+
/** "Confirm plan" or "Try again" while the job waits for the person. */
|
|
182
|
+
confirm: {
|
|
183
|
+
label: string;
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
} | null;
|
|
186
|
+
/** Another page build in flight on the site, said beside Confirm (AGL-3765). */
|
|
187
|
+
building: string | null;
|
|
188
|
+
/** Why a stopped job has nothing to show. */
|
|
189
|
+
problem: string | null;
|
|
190
|
+
/** What the job spent so far. */
|
|
191
|
+
spent: string | null;
|
|
192
|
+
open: AiJobFollowOpen | null;
|
|
193
|
+
}
|
|
194
|
+
/** The follow card's view of one job, for the apps (the console's `AiJobFollow` and `AiJobPlan`). */
|
|
195
|
+
export declare function aiJobFollowView(job: AiJobSummary, options?: {
|
|
196
|
+
orgSlug?: string | null;
|
|
197
|
+
staff?: boolean;
|
|
198
|
+
buildInFlight?: boolean;
|
|
199
|
+
now?: number;
|
|
200
|
+
}): AiJobFollowView;
|