@aglyn/plugins-ai 1.0.0-beta.235 → 1.0.0-beta.237

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 (98) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +37 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +67 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  6. package/src/lib/components/ai-job-plan.component.js +37 -12
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-job-requests.d.ts +10 -0
  9. package/src/lib/components/ai-job-requests.js +12 -4
  10. package/src/lib/components/ai-job-requests.js.map +1 -1
  11. package/src/lib/components/ai-site-build-page.component.js +26 -7
  12. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +68 -22
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  16. package/src/lib/components/assist-build-card.component.js +58 -7
  17. package/src/lib/components/assist-build-card.component.js.map +1 -1
  18. package/src/lib/components/assist-jobs-drawer.component.js +54 -6
  19. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  20. package/src/lib/components/assist-panel.component.js +2 -1
  21. package/src/lib/components/assist-panel.component.js.map +1 -1
  22. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  23. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  24. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  25. package/src/lib/jobs/ai-job-build-step.js +50 -34
  26. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-layout-language.js +1 -1
  28. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-language.js +4 -0
  30. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  31. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  32. package/src/lib/jobs/ai-job-page-step.js +34 -3
  33. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +23 -0
  35. package/src/lib/jobs/ai-job-plan-step.js +75 -15
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-site-content.d.ts +34 -3
  38. package/src/lib/jobs/ai-job-site-content.js +132 -32
  39. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  40. package/src/lib/jobs/ai-job-site-step.js +45 -15
  41. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  43. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  44. package/src/lib/jobs/ai-jobs.d.ts +18 -1
  45. package/src/lib/jobs/ai-jobs.js +29 -13
  46. package/src/lib/jobs/ai-jobs.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +105 -18
  48. package/src/lib/jobs/ai-layout-stock-photos.js +356 -114
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/layout-language/ai-layout-compiler.d.ts +27 -0
  51. package/src/lib/layout-language/ai-layout-compiler.js +78 -11
  52. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  53. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  54. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-language.js +1 -1
  56. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-listings.d.ts +37 -1
  58. package/src/lib/layout-language/ai-layout-listings.js +66 -11
  59. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-pictures.d.ts +2 -1
  61. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  62. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  63. package/src/lib/model/ai-build-job.d.ts +31 -0
  64. package/src/lib/model/ai-build-job.js +86 -1
  65. package/src/lib/model/ai-build-job.js.map +1 -1
  66. package/src/lib/model/ai-build-progress.d.ts +8 -0
  67. package/src/lib/model/ai-build-progress.js +33 -0
  68. package/src/lib/model/ai-build-progress.js.map +1 -1
  69. package/src/lib/model/ai-credit-estimate.d.ts +182 -0
  70. package/src/lib/model/ai-credit-estimate.js +190 -0
  71. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  72. package/src/lib/model/ai-jobs.types.d.ts +15 -0
  73. package/src/lib/model/ai-jobs.types.js.map +1 -1
  74. package/src/lib/model/ai-site-job.d.ts +59 -22
  75. package/src/lib/model/ai-site-job.js +104 -23
  76. package/src/lib/model/ai-site-job.js.map +1 -1
  77. package/src/lib/model/ai-site-kinds.js +3 -3
  78. package/src/lib/model/ai-site-kinds.js.map +1 -1
  79. package/src/lib/runtime/ai-doctrine-validators.js +10 -1
  80. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  81. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  82. package/src/lib/runtime/ai-node-tree.js +26 -0
  83. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  84. package/src/lib/runtime/ai-palette.generated.js +539 -7
  85. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  86. package/src/lib/server/ai-jobs-resume.js +93 -35
  87. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  88. package/src/lib/server/ai-jobs-route.js +45 -4
  89. package/src/lib/server/ai-jobs-route.js.map +1 -1
  90. package/src/lib/server/assist-chat.js +4 -1
  91. package/src/lib/server/assist-chat.js.map +1 -1
  92. package/src/lib/server/assist-edit.js +3 -1
  93. package/src/lib/server/assist-edit.js.map +1 -1
  94. package/src/lib/server/assist-music.d.ts +36 -0
  95. package/src/lib/server/assist-music.js +36 -0
  96. package/src/lib/server/assist-music.js.map +1 -0
  97. package/src/lib/server.js +6 -0
  98. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-stock-photos.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 {\n pluginMediaIngest,\n type PluginMediaIngest,\n type PluginMediaIngestResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport {\n STOCK_PHOTO_QUERY_MAX_CHARS,\n stockPhotoProvider,\n stockPhotoSourceKey,\n type StockPhoto,\n type StockPhotoOrientation,\n type StockPhotoProvider,\n type StockPhotoSearchRequest,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport {\n aiLayoutSeedNumber,\n type AiLayoutPicturePhoto,\n type AiLayoutPictureSlot,\n type AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\n\n/**\n * STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).\n *\n * The source `aiResolveLayoutPictures` asks before it falls back to the\n * starter photos. It names no library: it asks core for the deployment's\n * stock photo provider (`core.stock-photos`) and for the media library's\n * server door (`core.media-ingest`), and builds no source at all when either\n * is missing, so a deployment without a key behaves exactly as before.\n *\n * For each slot, in document order:\n *\n * 1. **Search.** Plain words: the kind of business (the head of the site's\n * business type, \"yoga studio\" from \"a family-owned yoga studio in\n * Austin\") for the hero; the picture's own subject (the content words of\n * the alt text the model wrote, else its section's name) with the business\n * for the rest, and an about picture asks for people. The orientation and\n * a minimum size come from the slot's frame. The provider caches every\n * search for a day and keeps its own rate limit.\n * 2. **Pick.** Among the first hits not already on the page, by the job's\n * seed and the slot, so two sites built from the same words rarely open\n * with the same photo while one job always picks the same.\n * 3. **Keep.** A photo the site's library already holds (by its source key)\n * is reused; otherwise its bytes are downloaded from the library and\n * stored in the site's own media library as the member who started the\n * job, credited to its photographer. The page names the site's asset,\n * never the library's address.\n *\n * A slot that finds nothing, any error, a refusal from the library (its\n * storage band, say) or the time running out leaves the slot to a starter\n * photo. Nothing here throws, and none of it is an AI model call: photos cost\n * no AI credits.\n */\n\n/** Wall clock the whole page's photos may take, inside the step's own signal. */\nexport const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20_000\n\n/** Searches one page may ask, cached ones included. */\nexport const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8\n\n/** The hits a pick is made among. */\nexport const AI_LAYOUT_STOCK_PICK_WINDOW = 8\n\n/** The largest photo copied: inside one direct upload. */\nexport const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024\n\n/** Words that carry no subject. */\nconst STOPWORDS = new Set(\n (\n 'a an the and or of in on at to for with from by into onto over under near our your their my his her its ' +\n 'this that these those is are be being was were who whom which where while as about than then very just ' +\n 'small local best new top quality premium family-owned family owned independent professional ' +\n 'business company shop services service site website page photo picture image showing shows show ' +\n 'someone people person one two three some many several'\n ).split(' '),\n)\n\n/** Lowercase words, letters only, without stopwords. */\nfunction contentWords(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/https?:\\/\\/\\S+|\\S+@\\S+|\\+?\\d[\\d\\s().-]{5,}\\d/g, ' ')\n .replace(/[^a-zÀ-ɏ\\s-]/g, ' ')\n .split(/\\s+/)\n .map((word) => word.replace(/^-+|-+$/g, ''))\n .filter((word) => word.length > 1 && !STOPWORDS.has(word))\n}\n\n/**\n * The kind of business in a few words: the first clause of the business\n * type, before a place or an audience, its last three content words (the\n * head of an English noun phrase is at its end).\n */\nexport function aiStockBusinessWords(businessType: string): string {\n const clause = businessType\n .toLowerCase()\n .split(/[,;:.()]|\\s[-–—]\\s|\\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\\b/)[0]\n const words = contentWords(clause ?? '')\n return words.slice(-3).join(' ')\n}\n\n/** A picture's subject in a few words: its alt text's first content words, else its section's name. */\nexport function aiStockSubjectWords(alt: string, sectionName: string): string {\n const words = contentWords(alt)\n if (words.length) return words.slice(0, 3).join(' ')\n return contentWords(sectionName).slice(0, 2).join(' ')\n}\n\nconst clip = (query: string) => query.replace(/\\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim()\n\n/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */\nexport function aiStockOrientation(aspect: number): StockPhotoOrientation {\n if (aspect > 1.15) return 'horizontal'\n if (aspect < 0.87) return 'vertical'\n return 'any'\n}\n\n/** The searches a slot tries, in order, most specific first, each distinct. */\nexport function aiStockSearchesFor(\n slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>,\n words: { business: string; subject: string },\n): StockPhotoSearchRequest[] {\n const orientation = aiStockOrientation(slot.aspect)\n const size =\n slot.role === 'hero'\n ? { minWidth: 1600 }\n : orientation === 'vertical'\n ? { minHeight: 900 }\n : { minWidth: 900 }\n const queries =\n slot.role === 'hero'\n ? [words.business, `${words.business} ${words.subject}`]\n : slot.role === 'about'\n ? [`${words.business} ${words.subject}`, words.business]\n : [`${words.business} ${words.subject}`, words.subject, words.business]\n const seen = new Set<string>()\n const searches: StockPhotoSearchRequest[] = []\n for (const raw of queries) {\n const query = clip(raw)\n if (!query || seen.has(query)) continue\n seen.add(query)\n searches.push({\n query,\n orientation,\n ...size,\n ...(slot.role === 'about' ? { people: true } : {}),\n })\n }\n return searches\n}\n\n/** The pick among a search's hits: not on the page yet, among the first few, by the seed. */\nexport function aiStockPick(\n photos: readonly StockPhoto[],\n used: ReadonlySet<string>,\n seed: string,\n): StockPhoto | null {\n const fresh = photos.filter((photo) => !used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW)\n if (!fresh.length) return null\n return fresh[aiLayoutSeedNumber(seed) % fresh.length] ?? null\n}\n\nconst EXTENSIONS: Record<string, string> = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }\n\nexport interface AiLayoutStockPhotoSourceInput {\n hostId: string\n /** The member the photos are stored as: the job's creator. */\n uid: string\n /** The job's seed, as the starter photos turn by. */\n seed: string\n /** The site's business type, else the job's brief. */\n business: string\n /** The page's section names, by plan index. */\n sectionNames: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface AiLayoutStockPhotoDeps {\n provider?: () => StockPhotoProvider | null\n ingest?: () => PluginMediaIngest | null\n budgetMs?: number\n}\n\n/**\n * The source for one page, or `null` when this deployment has no stock\n * photo library or no media door, which leaves every slot to the starters.\n */\nexport function aiLayoutStockPhotoSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutStockPhotoDeps = {},\n): AiLayoutPictureSource | null {\n const provider = (deps.provider ?? stockPhotoProvider)()\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n if (!provider || !ingest || !input.hostId || !input.uid) return null\n const business = aiStockBusinessWords(input.business)\n return async (slots) => {\n const timeout = AbortSignal.timeout(deps.budgetMs ?? AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS)\n const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout\n const used = new Set<string>()\n let searches = 0\n let storing = true\n const photos: Array<AiLayoutPicturePhoto | null> = []\n for (const [index, slot] of slots.entries()) {\n if (signal.aborted) {\n photos.push(null)\n continue\n }\n const subject = aiStockSubjectWords(slot.alt, input.sectionNames[slot.sectionIndex] ?? '')\n let placed: AiLayoutPicturePhoto | null = null\n for (const request of aiStockSearchesFor(slot, { business, subject })) {\n if (placed || signal.aborted || searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) break\n searches += 1\n let found: StockPhoto[]\n try {\n found = (await provider.search(request, { signal }))?.photos ?? []\n } catch {\n found = []\n }\n const photo = aiStockPick(found, used, `${input.seed}:${index}:${request.query}`)\n if (!photo) continue\n const sourceKey = stockPhotoSourceKey(photo)\n used.add(sourceKey)\n try {\n const kept = await ingest.findStockPhoto({ hostId: input.hostId, sourceKey })\n if (kept) {\n placed = { src: kept.src, width: kept.width ?? photo.width, height: kept.height ?? photo.height }\n break\n }\n // A refusal to store (the storage band, a lockdown) holds for the\n // whole page, so the rest of it reuses what it can and copies nothing.\n if (!storing) continue\n const bytes = await provider.download(photo, { maxBytes: AI_LAYOUT_STOCK_MAX_BYTES, signal })\n if (!bytes || signal.aborted) continue\n const credit = provider.credit(photo)\n const stored = await ingest.ingest({\n hostId: input.hostId,\n uid: input.uid,\n fileName: `${photo.provider}-${photo.id}.${EXTENSIONS[bytes.contentType] ?? 'jpg'}`,\n contentType: bytes.contentType,\n bytes: bytes.bytes,\n alt: slot.alt,\n description: credit.text,\n stockPhoto: {\n key: sourceKey,\n provider: photo.provider,\n providerLabel: credit.providerLabel,\n id: photo.id,\n pageUrl: photo.pageUrl,\n photographer: photo.photographer,\n ...(photo.photographerUrl ? { photographerUrl: photo.photographerUrl } : {}),\n license: credit.license,\n licenseUrl: credit.licenseUrl,\n attributionRequired: credit.attributionRequired,\n query: request.query,\n },\n })\n if (stored.ok) {\n placed = { src: stored.src, width: stored.width ?? photo.width, height: stored.height ?? photo.height }\n } else {\n storing = false\n const refusal = stored as Extract<PluginMediaIngestResult, { ok: false }>\n console.warn('ai layout pictures: the media library refused a stock photo', {\n status: refusal.status,\n reason: refusal.reason,\n })\n }\n } catch (error) {\n console.warn('ai layout pictures: a stock photo could not be kept', { error: String(error) })\n }\n }\n photos.push(placed)\n }\n return photos\n }\n}\n"],"names":["pluginMediaIngest","STOCK_PHOTO_QUERY_MAX_CHARS","stockPhotoProvider","stockPhotoSourceKey","aiLayoutSeedNumber","AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS","AI_LAYOUT_STOCK_SEARCHES_PER_PAGE","AI_LAYOUT_STOCK_PICK_WINDOW","AI_LAYOUT_STOCK_MAX_BYTES","STOPWORDS","Set","split","contentWords","text","toLowerCase","replace","map","word","filter","length","has","aiStockBusinessWords","businessType","clause","words","slice","join","aiStockSubjectWords","alt","sectionName","clip","query","trim","aiStockOrientation","aspect","aiStockSearchesFor","slot","orientation","size","role","minWidth","minHeight","queries","business","subject","seen","searches","raw","add","push","people","aiStockPick","photos","used","seed","fresh","photo","EXTENSIONS","aiLayoutStockPhotoSource","input","deps","provider","ingest","hostId","uid","slots","timeout","AbortSignal","budgetMs","signal","any","storing","index","entries","aborted","sectionNames","sectionIndex","placed","request","found","search","sourceKey","kept","findStockPhoto","src","width","height","bytes","download","maxBytes","credit","stored","fileName","id","contentType","description","stockPhoto","key","providerLabel","pageUrl","photographer","photographerUrl","license","licenseUrl","attributionRequired","ok","refusal","console","warn","status","reason","error","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-stock-photos.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 {\n pluginMediaIngest,\n type PluginMediaIngest,\n type PluginMediaIngestResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport {\n STOCK_PHOTO_QUERY_MAX_CHARS,\n stockPhotoProvider,\n stockPhotoSourceKey,\n type StockPhoto,\n type StockPhotoOrientation,\n type StockPhotoProvider,\n type StockPhotoSearchRequest,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport {\n aiLayoutSeedNumber,\n type AiLayoutPicturePhoto,\n type AiLayoutPictureSlot,\n type AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\n\n/**\n * STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).\n *\n * The source `aiResolveLayoutPictures` asks before it falls back to the\n * starter photos. It names no library: it asks core for the deployment's\n * stock photo provider (`core.stock-photos`) and for the media library's\n * server door (`core.media-ingest`), and builds no source at all when either\n * is missing, so a deployment without a key behaves exactly as before.\n *\n * For each slot, in document order:\n *\n * 1. **Search.** Plain words. The hero asks for the kind of business (the\n * head of the site's business type, \"yoga studio\" from \"a family-owned\n * yoga studio in Austin\"). Any other picture asks for its SUBJECT: the\n * noun phrase that opens the alt text the model wrote, its filler\n * adjectives dropped (\"serving bowl\" from \"A beautiful hand-thrown serving\n * bowl on a linen cloth\"), qualified by the site's craft where the phrase\n * does not already say it (\"serving bowl ceramic\" for a ceramics studio).\n * An about picture asks for people. The orientation and a minimum size\n * come from the slot's frame. The provider caches every search for a day\n * and keeps its own rate limit; one page never asks the same words twice.\n * 2. **Rank.** A picture of a thing must show the thing: every hit is scored\n * by how many of the subject's words its tags, its description and its\n * page's address name (singular and plural alike, the head noun counting\n * double), and a hit naming none of them never fills a picture of a\n * thing. The live Juniper Clay start put crackers under \"Serving bowl\"\n * and plants under \"Espresso cup\". Among the best-scored hits the job's\n * seed picks, so two sites built from the same words rarely open with the\n * same photo while one job always picks the same.\n * 3. **Never twice.** A photo is placed once per job: not twice on a page,\n * and not on a page when another page (or another pass, in this process)\n * of the same job already placed it — the maker's portrait in About is\n * never a vase in Selected work. The caller hands in what the job's other\n * pages hold (`avoid`); a pass repeated over its own page keeps its own.\n * 4. **Fall back.** A picture of a thing nothing relevant answered asks once\n * more for the place itself (\"ceramics studio interior\"), a neutral photo\n * any caption sits under, before it is left to a starter photo.\n * 5. **Keep.** A photo the site's library already holds (by its source key)\n * is reused; otherwise its bytes are downloaded from the library and\n * stored in the site's own media library as the member who started the\n * job, credited to its photographer. The page names the site's asset,\n * never the library's address.\n *\n * A slot that finds nothing, any error, a refusal from the library (its\n * storage band, say) or the time running out leaves the slot to a starter\n * photo. Nothing here throws, and none of it is an AI model call: photos cost\n * no AI credits.\n */\n\n/** Wall clock the whole page's photos may take, inside the step's own signal. */\nexport const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20_000\n\n/** Searches one page may send to the provider; a repeated query is answered from the page's own memory. */\nexport const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 12\n\n/** The hits a pick is made among. */\nexport const AI_LAYOUT_STOCK_PICK_WINDOW = 8\n\n/** The ranked hits one search may try to place before the slot moves to its next search. */\nconst AI_LAYOUT_STOCK_TRIES_PER_SEARCH = 3\n\n/** The largest photo copied: inside one direct upload. */\nexport const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024\n\n/**\n * The unit job input naming the screen drafts the job's other pages were\n * written to, whose photos a page does not place again (AGL-3660).\n */\nexport const AI_STOCK_PHOTO_PAGES_INPUT = 'stockPhotoPages'\n\n/** Words that carry no subject. */\nconst STOPWORDS = new Set(\n (\n 'a an the and or of in on at to for with from by into onto over under near our your their my his her its ' +\n 'this that these those is are be being was were who whom which where while as about than then very just ' +\n 'small local best new top quality premium family-owned family owned independent professional ' +\n 'business company shop services service site website page photo picture image showing shows show ' +\n 'someone people person one two three some many several'\n ).split(' '),\n)\n\n/**\n * Words that describe a picture's look, not what it shows, dropped from a\n * subject. A word that is as often a noun (\"light\", \"set\") is not one.\n */\nconst FILLER = new Set(\n (\n 'beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' +\n 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' +\n 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt finished styled perfect ' +\n 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' +\n 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' +\n 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' +\n 'single pair collection assortment selection group few various array range large tiny big little ' +\n 'white black grey gray beige cream ivory brown tan blue green red pink yellow orange purple speckled ' +\n 'gentle airy inviting welcoming cheerful happy smiling friendly'\n ).split(' '),\n)\n\n/** The nouns that name a place of business, not its craft: \"studio\" in \"ceramics studio\". */\nconst VENUES = new Set(\n (\n 'studio studios shop shops store stores boutique company co workshop gallery atelier agency brand practice ' +\n 'salon house collective lab market services service firm clinic center centre parlor parlour bar ' +\n 'class classes lessons school academy'\n ).split(' '),\n)\n\n/** Lowercase words, letters only, without stopwords. */\nfunction contentWords(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/https?:\\/\\/\\S+|\\S+@\\S+|\\+?\\d[\\d\\s().-]{5,}\\d/g, ' ')\n .replace(/[^a-zÀ-ɏ\\s-]/g, ' ')\n .split(/\\s+/)\n .map((word) => word.replace(/^-+|-+$/g, ''))\n .filter((word) => word.length > 1 && !STOPWORDS.has(word))\n}\n\n/**\n * A word's singular, for matching only: \"bowls\" and \"bowl\", \"vases\" and\n * \"vase\", \"berries\" and \"berry\", \"dishes\" and \"dish\" meet. Both sides of a\n * comparison are reduced the same way, so an odd stem still matches itself.\n */\nexport function aiStockStem(word: string): string {\n const lower = word.toLowerCase()\n if (lower.length > 4 && lower.endsWith('ies')) return `${lower.slice(0, -3)}y`\n if (lower.length > 4 && /(?:ches|shes|sses|xes|zes)$/.test(lower)) return lower.slice(0, -2)\n if (lower.length > 3 && lower.endsWith('s') && !/(?:ss|us|is)$/.test(lower)) return lower.slice(0, -1)\n return lower\n}\n\n/** A text's words as stems; a hyphenated word counts as its parts. */\nfunction stems(text: string): Set<string> {\n const out = new Set<string>()\n for (const word of text.toLowerCase().split(/[^a-zÀ-ɏ]+/)) {\n if (word.length > 1) out.add(aiStockStem(word))\n }\n return out\n}\n\n/** Where a noun phrase ends: punctuation, a preposition, a conjunction. */\nconst CLAUSE_BREAK =\n /[,;:.()]|\\s[-–—]\\s|\\b(?:with|on|in|at|against|beside|next|near|under|over|by|from|for|during|while|atop|inside|across|beneath|through|into|onto|amid|among|and|of|that|which|who)\\b|&/\n\n/**\n * The kind of business in a few words: the first clause of the business\n * type, before a place or an audience, its last three content words (the\n * head of an English noun phrase is at its end).\n */\nexport function aiStockBusinessWords(businessType: string): string {\n const clause = businessType\n .toLowerCase()\n .split(/[,;:.()]|\\s[-–—]\\s|\\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\\b/)[0]\n const words = contentWords(clause ?? '')\n return words.slice(-3).join(' ')\n}\n\n/**\n * The business's craft in one word: the last of its words that is not the\n * place it is done in, singular — \"ceramic\" from \"small-batch ceramics\n * studio\", \"yoga\" from \"yoga studio\". Empty when the business is only a\n * place (\"a shop\").\n */\nexport function aiStockCraftWords(business: string): string {\n const words = business.split(/\\s+/).filter((word) => word && !VENUES.has(word) && !FILLER.has(word))\n const craft = words[words.length - 1]\n return craft ? aiStockStem(craft) : ''\n}\n\n/** A phrase's content words without its filler; all filler, and nothing is left. */\nfunction subjectOf(words: readonly string[]): string[] {\n return words.filter((word) => !FILLER.has(word))\n}\n\n/**\n * A picture's subject in a few words (AGL-3660): the noun phrase its alt\n * text opens with — its first clause holding more than filler, cut where a\n * preposition starts the setting, filler dropped, its last three words (the\n * head noun is the last) — else its section's name.\n */\nexport function aiStockSubjectWords(alt: string, sectionName: string): string {\n for (const clause of alt.toLowerCase().split(CLAUSE_BREAK)) {\n const words = subjectOf(contentWords(clause ?? ''))\n if (words.length) return words.slice(-3).join(' ')\n }\n return subjectOf(contentWords(sectionName)).slice(0, 2).join(' ')\n}\n\nconst clip = (query: string) => query.replace(/\\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim()\n\n/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */\nexport function aiStockOrientation(aspect: number): StockPhotoOrientation {\n if (aspect > 1.15) return 'horizontal'\n if (aspect < 0.87) return 'vertical'\n return 'any'\n}\n\n/**\n * One search a slot tries. A gallery picture's searches must find a hit\n * naming its subject; a `neutral` one asks for the place itself, which any\n * caption sits under.\n */\nexport interface AiStockSearch extends StockPhotoSearchRequest {\n neutral?: boolean\n}\n\n/** The searches a slot tries, in order, most specific first, each distinct. */\nexport function aiStockSearchesFor(\n slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>,\n words: { business: string; subject: string; craft?: string },\n): AiStockSearch[] {\n const orientation = aiStockOrientation(slot.aspect)\n const size =\n slot.role === 'hero'\n ? { minWidth: 1600 }\n : orientation === 'vertical'\n ? { minHeight: 900 }\n : { minWidth: 900 }\n const craft = words.craft ?? aiStockCraftWords(words.business)\n const subjectStems = stems(words.subject)\n // The craft qualifies a subject that does not already say it.\n const qualified =\n craft && [...stems(craft)].some((stem) => !subjectStems.has(stem)) ? `${words.subject} ${craft}` : words.subject\n const place = words.business.split(/\\s+/).some((word) => VENUES.has(word))\n ? `${words.business} interior`\n : words.business\n const queries: Array<{ query: string; neutral?: boolean }> =\n slot.role === 'hero'\n ? [{ query: words.business }, { query: `${words.business} ${words.subject}` }]\n : slot.role === 'about'\n ? [{ query: `${words.business} ${words.subject}` }, { query: words.business }]\n : words.subject\n ? [{ query: qualified }, { query: words.subject }, { query: place, neutral: true }]\n : [{ query: place, neutral: true }]\n const seen = new Set<string>()\n const searches: AiStockSearch[] = []\n for (const raw of queries) {\n const query = clip(raw.query)\n if (!query || seen.has(query)) continue\n seen.add(query)\n searches.push({\n query,\n orientation,\n ...size,\n ...(slot.role === 'about' ? { people: true } : {}),\n ...(raw.neutral ? { neutral: true } : {}),\n })\n }\n return searches\n}\n\n/**\n * How well a hit names a subject (AGL-3660): each subject word its tags, its\n * description or its page's address names counts once, the head noun (the\n * subject's last word) twice, and a craft word a quarter, as a tiebreak.\n * Zero when it names none of the subject.\n */\nexport function aiStockRelevance(\n photo: Pick<StockPhoto, 'tags' | 'alt' | 'pageUrl'>,\n terms: { subject: string; craft?: string },\n): number {\n const subject = terms.subject.split(/\\s+/).filter(Boolean).map(aiStockStem)\n if (!subject.length) return 0\n let slug: string\n try {\n slug = new URL(photo.pageUrl).pathname\n } catch {\n slug = ''\n }\n const named = stems([...(photo.tags ?? []), photo.alt ?? '', slug].join(' '))\n const head = subject[subject.length - 1] as string\n let score = 0\n for (const word of new Set(subject)) {\n if (named.has(word)) score += word === head ? 2 : 1\n }\n if (score === 0) return 0\n for (const word of new Set((terms.craft ?? '').split(/\\s+/).filter(Boolean).map(aiStockStem))) {\n if (named.has(word)) score += 0.25\n }\n return score\n}\n\nexport interface AiStockRankOptions {\n /** The words a hit is scored by; none, and every hit scores alike. */\n subject?: string\n craft?: string\n /** A hit naming none of the subject is no candidate at all. */\n strict?: boolean\n}\n\n/**\n * A search's hits in the order they are tried (AGL-3660): those not placed\n * yet, among the first few, best-scored first; the seed turns the hits that\n * score alike, so two jobs differ and one job repeats itself.\n */\nexport function aiStockRank(\n photos: readonly StockPhoto[],\n used: ReadonlySet<string>,\n seed: string,\n options: AiStockRankOptions = {},\n): StockPhoto[] {\n const subject = options.subject ?? ''\n const scored = photos\n .filter((photo) => !used.has(stockPhotoSourceKey(photo)))\n .slice(0, AI_LAYOUT_STOCK_PICK_WINDOW)\n .map((photo) => ({\n photo,\n score: subject ? aiStockRelevance(photo, { subject, ...(options.craft ? { craft: options.craft } : {}) }) : 0,\n }))\n .filter((entry) => !options.strict || !subject || entry.score > 0)\n if (!scored.length) return []\n const turn = aiLayoutSeedNumber(seed)\n const ranked: StockPhoto[] = []\n for (const score of [...new Set(scored.map((entry) => entry.score))].sort((a, b) => b - a)) {\n const tier = scored.filter((entry) => entry.score === score)\n const start = turn % tier.length\n for (const entry of [...tier.slice(start), ...tier.slice(0, start)]) ranked.push(entry.photo)\n }\n return ranked\n}\n\n/** The pick among a search's hits: the first {@link aiStockRank} would try, or `null`. */\nexport function aiStockPick(\n photos: readonly StockPhoto[],\n used: ReadonlySet<string>,\n seed: string,\n options: AiStockRankOptions = {},\n): StockPhoto | null {\n return aiStockRank(photos, used, seed, options)[0] ?? null\n}\n\n/**\n * The library photos a stored page already shows: every `image` whose\n * source is the site's media reference. Starter photos are the platform's,\n * not the job's, and are not listed.\n */\nexport function aiStockPlacedSrcs(nodes: NodesMap | null | undefined): string[] {\n const map = (nodes ?? {}) as unknown as Record<string, { componentId?: string; props?: Record<string, unknown> }>\n const out: string[] = []\n for (const node of Object.values(map)) {\n const src = node?.componentId === 'image' ? node.props?.['src'] : null\n if (typeof src === 'string' && src.startsWith('media:')) out.push(src)\n }\n return out\n}\n\n/**\n * What this process has placed for each job, by source key and asset src,\n * with the scope (the page's seed) that placed it (AGL-3660). It spans the\n * passes of one job that run here, so a page, the products or a post built\n * later does not place what an earlier one did, while a pass repeated over\n * its own page keeps its own photos. Bounded; the oldest job goes first.\n */\nconst JOB_PHOTOS = new Map<string, Map<string, string>>()\nconst JOB_PHOTOS_MAX = 200\n\nfunction jobPhotos(key: string | null): Map<string, string> | null {\n if (!key) return null\n let placed = JOB_PHOTOS.get(key)\n if (!placed) {\n placed = new Map()\n JOB_PHOTOS.set(key, placed)\n while (JOB_PHOTOS.size > JOB_PHOTOS_MAX) {\n const oldest = JOB_PHOTOS.keys().next().value\n if (oldest === undefined) break\n JOB_PHOTOS.delete(oldest)\n }\n }\n return placed\n}\n\n/** Forgets every job's placed photos; for specs. */\nexport function aiStockForgetJobPhotos(): void {\n JOB_PHOTOS.clear()\n}\n\nconst EXTENSIONS: Record<string, string> = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }\n\nexport interface AiLayoutStockPhotoSourceInput {\n hostId: string\n /** The member the photos are stored as: the job's creator. */\n uid: string\n /** The job's seed, as the starter photos turn by; also the scope a photo is placed under. */\n seed: string\n /** The site's business type, else the job's brief. */\n business: string\n /** The page's section names, by plan index. */\n sectionNames: readonly string[]\n /**\n * The job the member started (`aiOriginJobId`), so a photo is placed once\n * across its pages. Absent, only the page itself is kept free of repeats.\n */\n jobId?: string\n /** Asset srcs or source keys the job's other pages already show. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface AiLayoutStockPhotoDeps {\n provider?: () => StockPhotoProvider | null\n ingest?: () => PluginMediaIngest | null\n budgetMs?: number\n}\n\n/**\n * The source for one page, or `null` when this deployment has no stock\n * photo library or no media door, which leaves every slot to the starters.\n */\nexport function aiLayoutStockPhotoSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutStockPhotoDeps = {},\n): AiLayoutPictureSource | null {\n const provider = (deps.provider ?? stockPhotoProvider)()\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n if (!provider || !ingest || !input.hostId || !input.uid) return null\n const business = aiStockBusinessWords(input.business)\n const craft = aiStockCraftWords(business)\n return async (slots) => {\n const timeout = AbortSignal.timeout(deps.budgetMs ?? AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS)\n const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout\n const scope = input.seed\n const job = jobPhotos(input.jobId ? `${input.hostId}:${input.jobId}` : null)\n const avoid = new Set(input.avoid ?? [])\n /** Placed elsewhere in the job: by another page in this process, or one the caller read. */\n const elsewhere = (key: string) => avoid.has(key) || (job?.has(key) === true && job.get(key) !== scope)\n /** Source keys this page has placed or turned down. */\n const used = new Set<string>()\n /** Asset srcs this page has placed. */\n const placedSrcs = new Set<string>()\n const answers = new Map<string, StockPhoto[]>()\n let searches = 0\n let storing = true\n const remember = (sourceKey: string, src: string) => {\n placedSrcs.add(src)\n job?.set(sourceKey, scope)\n job?.set(src, scope)\n }\n /** A search's hits; the same words twice are answered from memory, and `null` once the page's searches are spent. */\n const search = async (wanted: AiStockSearch): Promise<StockPhoto[] | null> => {\n const request: StockPhotoSearchRequest = { ...wanted }\n delete (request as AiStockSearch).neutral\n const key = JSON.stringify([request.query, request.orientation, request.people === true])\n const known = answers.get(key)\n if (known) return known\n if (searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) return null\n searches += 1\n let found: StockPhoto[]\n try {\n found = (await provider.search(request, { signal }))?.photos ?? []\n } catch {\n found = []\n }\n answers.set(key, found)\n return found\n }\n /** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */\n const keep = async (\n photo: StockPhoto,\n slot: AiLayoutPictureSlot,\n query: string,\n ): Promise<AiLayoutPicturePhoto | null> => {\n const sourceKey = stockPhotoSourceKey(photo)\n used.add(sourceKey)\n try {\n const kept = await ingest.findStockPhoto({ hostId: input.hostId, sourceKey })\n if (kept) {\n // Held because a slot of this page or another page of the job shows it.\n if (placedSrcs.has(kept.src) || elsewhere(kept.src)) return null\n remember(sourceKey, kept.src)\n return { src: kept.src, width: kept.width ?? photo.width, height: kept.height ?? photo.height }\n }\n // A refusal to store (the storage band, a lockdown) holds for the\n // whole page, so the rest of it reuses what it can and copies nothing.\n if (!storing) return null\n const bytes = await provider.download(photo, { maxBytes: AI_LAYOUT_STOCK_MAX_BYTES, signal })\n if (!bytes || signal.aborted) return null\n const credit = provider.credit(photo)\n const stored = await ingest.ingest({\n hostId: input.hostId,\n uid: input.uid,\n fileName: `${photo.provider}-${photo.id}.${EXTENSIONS[bytes.contentType] ?? 'jpg'}`,\n contentType: bytes.contentType,\n bytes: bytes.bytes,\n alt: slot.alt,\n description: credit.text,\n stockPhoto: {\n key: sourceKey,\n provider: photo.provider,\n providerLabel: credit.providerLabel,\n id: photo.id,\n pageUrl: photo.pageUrl,\n photographer: photo.photographer,\n ...(photo.photographerUrl ? { photographerUrl: photo.photographerUrl } : {}),\n license: credit.license,\n licenseUrl: credit.licenseUrl,\n attributionRequired: credit.attributionRequired,\n query,\n },\n })\n if (stored.ok) {\n remember(sourceKey, stored.src)\n return { src: stored.src, width: stored.width ?? photo.width, height: stored.height ?? photo.height }\n }\n storing = false\n const refusal = stored as Extract<PluginMediaIngestResult, { ok: false }>\n console.warn('ai layout pictures: the media library refused a stock photo', {\n status: refusal.status,\n reason: refusal.reason,\n })\n } catch (error) {\n console.warn('ai layout pictures: a stock photo could not be kept', { error: String(error) })\n }\n return null\n }\n const photos: Array<AiLayoutPicturePhoto | null> = []\n for (const [index, slot] of slots.entries()) {\n if (signal.aborted) {\n photos.push(null)\n continue\n }\n const subject = aiStockSubjectWords(slot.alt, input.sectionNames[slot.sectionIndex] ?? '')\n let placed: AiLayoutPicturePhoto | null = null\n for (const request of aiStockSearchesFor(slot, { business, subject, craft })) {\n if (placed || signal.aborted) break\n const found = await search(request)\n if (found === null) break\n // A gallery picture names a thing, and only a hit naming it fills it;\n // a hero, an about picture and the neutral search rank, unfiltered.\n const blocked = new Set(used)\n for (const hit of found) {\n const key = stockPhotoSourceKey(hit)\n if (elsewhere(key)) blocked.add(key)\n }\n const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, {\n subject: request.neutral ? craft || business : subject,\n craft,\n strict: slot.role === 'gallery' && !request.neutral,\n })\n for (const photo of ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SEARCH)) {\n if (signal.aborted) break\n placed = await keep(photo, slot, request.query)\n if (placed) break\n }\n }\n photos.push(placed)\n }\n return photos\n }\n}\n"],"names":["pluginMediaIngest","STOCK_PHOTO_QUERY_MAX_CHARS","stockPhotoProvider","stockPhotoSourceKey","aiLayoutSeedNumber","AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS","AI_LAYOUT_STOCK_SEARCHES_PER_PAGE","AI_LAYOUT_STOCK_PICK_WINDOW","AI_LAYOUT_STOCK_TRIES_PER_SEARCH","AI_LAYOUT_STOCK_MAX_BYTES","AI_STOCK_PHOTO_PAGES_INPUT","STOPWORDS","Set","split","FILLER","VENUES","contentWords","text","toLowerCase","replace","map","word","filter","length","has","aiStockStem","lower","endsWith","slice","test","stems","out","add","CLAUSE_BREAK","aiStockBusinessWords","businessType","clause","words","join","aiStockCraftWords","business","craft","subjectOf","aiStockSubjectWords","alt","sectionName","clip","query","trim","aiStockOrientation","aspect","aiStockSearchesFor","slot","orientation","size","role","minWidth","minHeight","subjectStems","subject","qualified","some","stem","place","queries","neutral","seen","searches","raw","push","people","aiStockRelevance","photo","terms","Boolean","slug","URL","pageUrl","pathname","named","tags","head","score","aiStockRank","photos","used","seed","options","scored","entry","strict","turn","ranked","sort","a","b","tier","start","aiStockPick","aiStockPlacedSrcs","nodes","node","Object","values","src","componentId","props","startsWith","JOB_PHOTOS","Map","JOB_PHOTOS_MAX","jobPhotos","key","placed","get","set","oldest","keys","next","value","undefined","delete","aiStockForgetJobPhotos","clear","EXTENSIONS","aiLayoutStockPhotoSource","input","deps","provider","ingest","hostId","uid","slots","timeout","AbortSignal","budgetMs","signal","any","scope","job","jobId","avoid","elsewhere","placedSrcs","answers","storing","remember","sourceKey","search","wanted","request","JSON","stringify","known","found","keep","kept","findStockPhoto","width","height","bytes","download","maxBytes","aborted","credit","stored","fileName","id","contentType","description","stockPhoto","providerLabel","photographer","photographerUrl","license","licenseUrl","attributionRequired","ok","refusal","console","warn","status","reason","error","String","index","entries","sectionNames","sectionIndex","blocked","hit"],"mappings":"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"}
@@ -181,11 +181,38 @@ export type Room = 'full' | 'half' | 'narrow';
181
181
  /** A column of blocks, top to bottom, with buttons side by side; `null` when nothing in it compiles. */
182
182
  export declare function compileFlow(scope: SectionScope, blocks: readonly AiLayoutBlock[], room: Room): string | null;
183
183
  export declare function destinationProps(destination: AiLayoutDestination): Record<string, unknown>;
184
+ /**
185
+ * A picture slot: a soft frame at a stock shape showing an icon until the
186
+ * owner places the picture, which then fills it. Its description is its alt
187
+ * text (rule 9), and no source is set here: the compiler stays pure, and
188
+ * `ai-layout-pictures.ts` fills the slot with a photo the site serves itself
189
+ * once the page is checked.
190
+ */
191
+ /**
192
+ * The site kinds whose galleries open in a lightbox by default (AGL-3717): a
193
+ * portfolio's or a photographer's work is looked at, not skimmed, and a
194
+ * visitor who presses a picture expects to see it large and step through the
195
+ * rest. Every other kind opens a picture only where the design says so.
196
+ */
197
+ export declare const AI_LIGHTBOX_GALLERY_KINDS: ReadonlySet<string>;
198
+ /** The `to` that opens an image block's picture in a lightbox (AGL-3717). */
199
+ export declare const AI_LAYOUT_LIGHTBOX_TO = "lightbox";
200
+ /** Whether this page's galleries open in a lightbox by default. */
201
+ export declare function aiOpensGalleriesInLightbox(page: {
202
+ design: PageScope['design'];
203
+ }): boolean;
184
204
  /** The most pictures a designed page carries, the model's and the stand-ins together. */
185
205
  export declare const AI_LAYOUT_MAX_DESIGN_PICTURES = 8;
186
206
  /** The tokens a post's card binds, filled per post when the page renders. */
187
207
  export declare const AI_LAYOUT_POST_CARD_TOKENS: readonly ["{{entry.title}}", "{{entry.excerpt}}", "{{entry.date}}", "{{entry.author}}", "{{entry.url}}", "{{entry.coverImage}}"];
188
208
  /** The tokens a post's card puts in a link or a picture, which the page's store admits whole. */
189
209
  export declare const AI_LAYOUT_POST_ADDRESS_TOKENS: readonly string[];
210
+ /** What a store's Product grid says while the store lists nothing yet (AGL-3676). */
211
+ export declare const AI_LAYOUT_STORE_EMPTY: {
212
+ readonly title: "New pieces are on the way";
213
+ readonly text: "Our first pieces are being finished now. Get in touch and we will let you know the moment they land.";
214
+ /** Said where the site has no page to get in touch on. */
215
+ readonly textAlone: "Our first pieces are being finished now. Check back soon.";
216
+ };
190
217
  /** The id the compiled page's root is stored under. */
191
218
  export declare const AI_LAYOUT_PAGE_ROOT_ID = "_@_";
@@ -819,8 +819,39 @@ export function destinationProps(destination) {
819
819
  * text (rule 9), and no source is set here: the compiler stays pure, and
820
820
  * `ai-layout-pictures.ts` fills the slot with a photo the site serves itself
821
821
  * once the page is checked.
822
- */ function image(scope, block, room) {
822
+ */ /**
823
+ * The site kinds whose galleries open in a lightbox by default (AGL-3717): a
824
+ * portfolio's or a photographer's work is looked at, not skimmed, and a
825
+ * visitor who presses a picture expects to see it large and step through the
826
+ * rest. Every other kind opens a picture only where the design says so.
827
+ */ export const AI_LIGHTBOX_GALLERY_KINDS = new Set([
828
+ 'portfolio',
829
+ 'photography'
830
+ ]);
831
+ /** The `to` that opens an image block's picture in a lightbox (AGL-3717). */ export const AI_LAYOUT_LIGHTBOX_TO = 'lightbox';
832
+ /** Whether this page's galleries open in a lightbox by default. */ export function aiOpensGalleriesInLightbox(page) {
833
+ var _page_design;
834
+ const kind = (_page_design = page.design) == null ? void 0 : _page_design.input.kind;
835
+ return Boolean(kind && AI_LIGHTBOX_GALLERY_KINDS.has(kind));
836
+ }
837
+ /**
838
+ * The Image props that make a picture open in a lightbox: as one gallery
839
+ * with the section's other pictures, named for the section, and with the
840
+ * item's title as its caption. The Image element reads exactly these.
841
+ */ function lightboxProps(scope, caption) {
842
+ var _scope_page_plan_sections_scope_index;
843
+ const name = aiLayoutFitText(scope.heading, 'alt') || aiLayoutFitText((_scope_page_plan_sections_scope_index = scope.page.plan.sections[scope.index]) == null ? void 0 : _scope_page_plan_sections_scope_index.name, 'alt') || `Gallery ${scope.index + 1}`;
844
+ const words = aiLayoutFitText(caption, 'alt');
845
+ return _extends({
846
+ lightbox: true,
847
+ lightboxGallery: name
848
+ }, words ? {
849
+ lightboxCaption: words
850
+ } : {});
851
+ }
852
+ function image(scope, block, room) {
823
853
  var _aiIconOfWord;
854
+ var _block_to;
824
855
  const { page } = scope;
825
856
  const alt = aiLayoutFitText(block.text, 'alt');
826
857
  if (!alt) return null;
@@ -840,10 +871,13 @@ export function destinationProps(destination) {
840
871
  }, {
841
872
  color: 'primary.main'
842
873
  }, null, 'imageIcon');
843
- const picture = tree.add('image', {
874
+ // `to: lightbox` opens the picture large (AGL-3717); one picture is its
875
+ // own gallery, so it opens alone unless the section has more.
876
+ const opensLarge = ((_block_to = block.to) == null ? void 0 : _block_to.trim().toLowerCase()) === AI_LAYOUT_LIGHTBOX_TO;
877
+ const picture = tree.add('image', _extends({
844
878
  alt,
845
879
  objectFit: 'cover'
846
- }, {
880
+ }, opensLarge ? lightboxProps(scope) : {}), {
847
881
  position: 'absolute',
848
882
  top: 0,
849
883
  left: 0,
@@ -1907,12 +1941,15 @@ function instances(scope, placed, items, perRow) {
1907
1941
  const span = !mosaic ? 12 / perRow : lone ? 12 : wide ? 7 : 5;
1908
1942
  const aspect = !mosaic ? variant === 'articles' ? '3 / 2' : portrait ? '4 / 5' : '4 / 3' : lone ? '21 / 9' : wide ? '4 / 3' : '4 / 5';
1909
1943
  // The picture is its own frame: its shape its one style, so a grid of them repeats no inline style.
1944
+ // A portfolio's or a photographer's gallery opens in a lightbox, its
1945
+ // pictures one gallery with the item's title as each caption (AGL-3717).
1946
+ const opens = variant === 'pictures' && aiOpensGalleriesInLightbox(page);
1910
1947
  const photo = designImage(scope, {
1911
1948
  alt: standInAlt(scope, item.title || item.text),
1912
1949
  given: false
1913
- }, {
1950
+ }, _extends({
1914
1951
  width: '100%'
1915
- }, {
1952
+ }, opens ? lightboxProps(scope, item.title) : {}), {
1916
1953
  aspectRatio: {
1917
1954
  xs: '4 / 3',
1918
1955
  md: aspect
@@ -1978,12 +2015,13 @@ function instances(scope, placed, items, perRow) {
1978
2015
  style: 'secondary'
1979
2016
  } : null;
1980
2017
  const index = listing.placements.find((placement)=>placement.role === 'index' && placement.screenId !== page.targets.pageId);
1981
- return index ? {
2018
+ if (!index) return null;
2019
+ return {
1982
2020
  kind: 'button',
1983
- text: 'Shop all',
2021
+ text: listing.kind === 'tracks' ? 'All music' : 'Shop all',
1984
2022
  to: `page:${index.screenId}`,
1985
2023
  style: 'secondary'
1986
- } : null;
2024
+ };
1987
2025
  }
1988
2026
  function listingSection(scope, blocks, placed) {
1989
2027
  var _ref;
@@ -2090,6 +2128,22 @@ function listingSection(scope, blocks, placed) {
2090
2128
  const shown = featured ? AI_LAYOUT_FEATURED_RECORDS[listing.kind] : 12;
2091
2129
  const element = AI_LAYOUT_LISTING_ELEMENTS[listing.kind];
2092
2130
  let id;
2131
+ if (listing.kind === 'tracks') {
2132
+ // An empty player (AGL-3716): its "add your tracks" state, which the owner
2133
+ // fills from their own media library. No source, ever — the section's
2134
+ // heading and words are the model's, the recordings the owner's.
2135
+ id = tree.add(element, null, null, null, 'player');
2136
+ page.settled.push({
2137
+ at: scope.at,
2138
+ what: 'an empty music player placed for the owner’s own tracks'
2139
+ });
2140
+ // The player is what the section shows: its tracks are the owner's to add.
2141
+ noted(scope, [
2142
+ id,
2143
+ id
2144
+ ]);
2145
+ return id;
2146
+ }
2093
2147
  if (listing.kind === 'products') {
2094
2148
  id = tree.add(element, _extends({
2095
2149
  source: 'all',
@@ -2098,11 +2152,19 @@ function listingSection(scope, blocks, placed) {
2098
2152
  }, featured ? {
2099
2153
  maxItems: String(shown)
2100
2154
  } : {
2101
- pageSize: String(shown)
2155
+ pageSize: String(shown),
2156
+ showSort: true,
2157
+ showCategories: true
2102
2158
  }, {
2103
2159
  cardStyle: 'photo',
2104
- emptyText: 'New products are on their way.'
2105
- }), null, null, 'products');
2160
+ // A store that opens before its first products are in says so, with a
2161
+ // way to hear when they land (AGL-3676): never placeholder products.
2162
+ emptyTitle: AI_LAYOUT_STORE_EMPTY.title,
2163
+ emptyText: listing.emptyAction ? AI_LAYOUT_STORE_EMPTY.text : AI_LAYOUT_STORE_EMPTY.textAlone
2164
+ }, listing.emptyAction ? {
2165
+ emptyActionLabel: listing.emptyAction.label,
2166
+ emptyActionHref: listing.emptyAction.href
2167
+ } : {}), null, null, 'products');
2106
2168
  } else {
2107
2169
  id = postsElement(scope, listing, featured ? shown : COLLECTION_INDEX_POSTS);
2108
2170
  }
@@ -2113,6 +2175,11 @@ function listingSection(scope, blocks, placed) {
2113
2175
  }, ()=>id));
2114
2176
  return id;
2115
2177
  }
2178
+ /** What a store's Product grid says while the store lists nothing yet (AGL-3676). */ export const AI_LAYOUT_STORE_EMPTY = {
2179
+ title: 'New pieces are on the way',
2180
+ text: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',
2181
+ /** Said where the site has no page to get in touch on. */ textAlone: 'Our first pieces are being finished now. Check back soon.'
2182
+ };
2116
2183
  /** The most posts a page of the site lists where it is the writing's own page. */ const COLLECTION_INDEX_POSTS = 9;
2117
2184
  /** The blog's posts as the site's design lists them: a grid of covers, or a ruled list with the dates. */ function postsElement(scope, listing, limit) {
2118
2185
  var _page_design;