@aglyn/plugins-ai 1.0.0-beta.257 → 1.0.0-beta.259

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 (94) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/assist-panel.component.js +21 -8
  3. package/src/lib/components/assist-panel.component.js.map +1 -1
  4. package/src/lib/insights/ai-figure-readers.js +15 -8
  5. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  6. package/src/lib/jobs/ai-job-build-step.d.ts +9 -0
  7. package/src/lib/jobs/ai-job-build-step.js +19 -3
  8. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  9. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  10. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  11. package/src/lib/jobs/ai-job-page-language.d.ts +33 -0
  12. package/src/lib/jobs/ai-job-page-language.js +46 -6
  13. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  14. package/src/lib/jobs/ai-job-page-sections.js +12 -1
  15. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  16. package/src/lib/jobs/ai-job-page-step.d.ts +14 -0
  17. package/src/lib/jobs/ai-job-page-step.js +79 -39
  18. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  19. package/src/lib/jobs/ai-job-seo-step.js +3 -2
  20. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-template-step.js +7 -1
  22. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  23. package/src/lib/jobs/ai-layout-stock-photos.d.ts +10 -5
  24. package/src/lib/jobs/ai-layout-stock-photos.js +89 -7
  25. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  26. package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.d.ts +21 -0
  27. package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.js +126 -0
  28. package/src/lib/jobs/fixtures/ai-stock-photo-copper-kettle-recordings.js.map +1 -0
  29. package/src/lib/layout-language/ai-layout-compiler.js +52 -52
  30. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  31. package/src/lib/layout-language/ai-layout-copy.d.ts +9 -5
  32. package/src/lib/layout-language/ai-layout-copy.js +22 -14
  33. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  34. package/src/lib/layout-language/ai-layout-frame.js +4 -4
  35. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  36. package/src/lib/layout-language/ai-layout-motion.d.ts +75 -0
  37. package/src/lib/layout-language/ai-layout-motion.js +172 -0
  38. package/src/lib/layout-language/ai-layout-motion.js.map +1 -0
  39. package/src/lib/layout-language/ai-layout-pictures.d.ts +44 -15
  40. package/src/lib/layout-language/ai-layout-pictures.js +29 -8
  41. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  42. package/src/lib/model/ai-build-job.js +14 -0
  43. package/src/lib/model/ai-build-job.js.map +1 -1
  44. package/src/lib/model/ai-host-publish-context.d.ts +6 -0
  45. package/src/lib/model/ai-host-publish-context.js +15 -2
  46. package/src/lib/model/ai-host-publish-context.js.map +1 -1
  47. package/src/lib/model/assist-follow-up.d.ts +35 -0
  48. package/src/lib/model/assist-follow-up.js +44 -4
  49. package/src/lib/model/assist-follow-up.js.map +1 -1
  50. package/src/lib/operator-alerts.js +1 -1
  51. package/src/lib/operator-alerts.js.map +1 -1
  52. package/src/lib/providers/anthropic.js +17 -4
  53. package/src/lib/providers/anthropic.js.map +1 -1
  54. package/src/lib/providers/cache-write-hour.d.ts +37 -0
  55. package/src/lib/providers/cache-write-hour.js +46 -0
  56. package/src/lib/providers/cache-write-hour.js.map +1 -0
  57. package/src/lib/providers/contract.d.ts +16 -3
  58. package/src/lib/providers/contract.js.map +1 -1
  59. package/src/lib/providers/routing.d.ts +13 -0
  60. package/src/lib/providers/routing.js +27 -11
  61. package/src/lib/providers/routing.js.map +1 -1
  62. package/src/lib/runtime/ai-dev-env.d.ts +13 -3
  63. package/src/lib/runtime/ai-dev-env.js +17 -3
  64. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  65. package/src/lib/runtime/ai-dev-replay.js +4 -2
  66. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  67. package/src/lib/runtime/ai-doctrine-validators.d.ts +70 -1
  68. package/src/lib/runtime/ai-doctrine-validators.js +332 -2
  69. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  70. package/src/lib/runtime/ai-doctrine.js +12 -4
  71. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  72. package/src/lib/runtime/ai-eval-live.js +1 -1
  73. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  74. package/src/lib/runtime/ai-eval.d.ts +1 -1
  75. package/src/lib/runtime/ai-eval.js +18 -1
  76. package/src/lib/runtime/ai-eval.js.map +1 -1
  77. package/src/lib/runtime/ai-placeholder-tokens.d.ts +70 -0
  78. package/src/lib/runtime/ai-placeholder-tokens.js +169 -0
  79. package/src/lib/runtime/ai-placeholder-tokens.js.map +1 -0
  80. package/src/lib/runtime/ai-repeated-items.js +5 -2
  81. package/src/lib/runtime/ai-repeated-items.js.map +1 -1
  82. package/src/lib/runtime/ai-runtime.js +2 -3
  83. package/src/lib/runtime/ai-runtime.js.map +1 -1
  84. package/src/lib/server/assist-chat.js +8 -3
  85. package/src/lib/server/assist-chat.js.map +1 -1
  86. package/src/lib/server/assist-edit.d.ts +21 -0
  87. package/src/lib/server/assist-edit.js +99 -21
  88. package/src/lib/server/assist-edit.js.map +1 -1
  89. package/src/lib/server/assist-view-context.d.ts +5 -1
  90. package/src/lib/server/assist-view-context.js +8 -3
  91. package/src/lib/server/assist-view-context.js.map +1 -1
  92. package/src/lib/usage/assist-usage.d.ts +2 -0
  93. package/src/lib/usage/assist-usage.js +8 -3
  94. package/src/lib/usage/assist-usage.js.map +1 -1
@@ -0,0 +1,172 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { applyElementMotion } from "@aglyn/aglyn/app-utils/element-motion";
18
+ /** Each mood's range. The seed picks one character from it. */ export const AI_SITE_MOTION_MOODS = {
19
+ calm: {
20
+ presets: [
21
+ 'fade'
22
+ ],
23
+ media: [
24
+ 'fade'
25
+ ],
26
+ eases: [
27
+ 'gentle-end',
28
+ 'gentle-both'
29
+ ],
30
+ duration: [
31
+ 650,
32
+ 800
33
+ ],
34
+ stagger: [
35
+ 100,
36
+ 120
37
+ ],
38
+ hero: [
39
+ 130,
40
+ 160
41
+ ]
42
+ },
43
+ bold: {
44
+ presets: [
45
+ 'slide-up'
46
+ ],
47
+ media: [
48
+ 'zoom-in'
49
+ ],
50
+ eases: [
51
+ 'overshoot'
52
+ ],
53
+ duration: [
54
+ 450,
55
+ 600
56
+ ],
57
+ stagger: [
58
+ 60,
59
+ 80
60
+ ],
61
+ hero: [
62
+ 80,
63
+ 100
64
+ ]
65
+ },
66
+ gallery: {
67
+ presets: [
68
+ 'fade',
69
+ 'slide-up'
70
+ ],
71
+ media: [
72
+ 'zoom-in'
73
+ ],
74
+ eases: [
75
+ 'smooth'
76
+ ],
77
+ duration: [
78
+ 600,
79
+ 750
80
+ ],
81
+ stagger: [
82
+ 80,
83
+ 110
84
+ ],
85
+ hero: [
86
+ 110,
87
+ 130
88
+ ]
89
+ },
90
+ standard: {
91
+ presets: [
92
+ 'slide-up'
93
+ ],
94
+ media: [
95
+ 'slide-up',
96
+ 'fade'
97
+ ],
98
+ eases: [
99
+ 'smooth'
100
+ ],
101
+ duration: [
102
+ 500,
103
+ 700
104
+ ],
105
+ stagger: [
106
+ 70,
107
+ 100
108
+ ],
109
+ hero: [
110
+ 100,
111
+ 130
112
+ ]
113
+ }
114
+ };
115
+ /** Each site kind's mood; a kind not listed is standard. */ export const AI_SITE_MOTION_KINDS = {
116
+ professional: 'calm',
117
+ wellness: 'calm',
118
+ yoga: 'calm',
119
+ beauty: 'calm',
120
+ blog: 'calm',
121
+ studio: 'bold',
122
+ fitness: 'bold',
123
+ music: 'bold',
124
+ events: 'bold',
125
+ portfolio: 'gallery',
126
+ photography: 'gallery'
127
+ };
128
+ /** The mood a kind of site moves in. */ export function aiSiteMotionMood(kind) {
129
+ return kind && AI_SITE_MOTION_KINDS[kind] || 'standard';
130
+ }
131
+ /**
132
+ * A small deterministic generator over a seed (mulberry32), mixed with its
133
+ * own constant so a site's motion does not move in step with its layout.
134
+ */ function random(seed) {
135
+ let state = (seed ^ 0x6a09e667) >>> 0;
136
+ return ()=>{
137
+ state = state + 0x6d2b79f5 >>> 0;
138
+ let t = state;
139
+ t = Math.imul(t ^ t >>> 15, t | 1);
140
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
141
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
142
+ };
143
+ }
144
+ const pick = (next, list)=>list[Math.floor(next() * list.length) % list.length];
145
+ /** A value in an inclusive range, on a step: 10 ms for a duration, 5 ms for a gap. */ const within = (next, [low, high], step)=>low + Math.floor(next() * (Math.floor((high - low) / step) + 1)) * step;
146
+ /** The motion character of a site: its kind's mood, turned by its seed. */ export function aiSiteMotion(input) {
147
+ const range = AI_SITE_MOTION_MOODS[aiSiteMotionMood(input.kind)];
148
+ const next = random(input.seed >>> 0);
149
+ return {
150
+ preset: pick(next, range.presets),
151
+ mediaPreset: pick(next, range.media),
152
+ ease: pick(next, range.eases),
153
+ durationMs: within(next, range.duration, 10),
154
+ staggerStepMs: within(next, range.stagger, 5),
155
+ heroStepMs: within(next, range.hero, 5)
156
+ };
157
+ }
158
+ /**
159
+ * A page the AI built, with its site's entrance motion written onto it —
160
+ * or the same node map, when there was nothing left to move.
161
+ */ export function aiWithSiteMotion(nodes, site, page = {}) {
162
+ var _page_sectionIds;
163
+ return applyElementMotion(nodes, _extends({
164
+ character: aiSiteMotion(site)
165
+ }, page.rootId ? {
166
+ rootId: page.rootId
167
+ } : {}, ((_page_sectionIds = page.sectionIds) == null ? void 0 : _page_sectionIds.length) ? {
168
+ sectionIds: page.sectionIds
169
+ } : {}));
170
+ }
171
+
172
+ //# sourceMappingURL=ai-layout-motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-motion.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 { AnimationEase, AnimationPreset } from '@aglyn/aglyn/app-utils/element-animation'\nimport {\n applyElementMotion,\n type ElementMotionCharacter,\n} from '@aglyn/aglyn/app-utils/element-motion'\n\n/**\n * How an AI-built site moves (AGL-3838): a small motion CHARACTER per kind of\n * site, turned by the site's design seed, which the platform's entrance pass\n * (`element-motion.ts`) writes onto every page the AI builds.\n *\n * Decided in code, never by the model: no prompt names a preset, no answer\n * carries one, and no credit is spent on it. The same site — same kind, same\n * seed — always moves the same way on every page, and two sites of one kind\n * are drawn apart by their seeds, the way their looks are.\n *\n * The moods, by the kinds `ai-site-kinds.ts` lists:\n * - CALM — practices, health, reading: a fade, on a gentle curve, slower.\n * - BOLD — studios, fitness, music, events: a rise with overshoot, and\n * pictures that zoom in.\n * - GALLERY — portfolios and photography: pictures zoom in, one after\n * another; the words fade or rise.\n * - STANDARD — everything else: a smooth rise.\n *\n * Every number stays inside the vocabulary's caps, and inside 400–800 ms for\n * a duration and 60–120 ms between staggered items: slower reads as a site\n * that is loading, faster as one that is twitching.\n */\n\nexport type AiSiteMotionMood = 'calm' | 'bold' | 'gallery' | 'standard'\n\ninterface AiSiteMotionRange {\n /** The word presets the seed picks among. */\n presets: readonly AnimationPreset[]\n /** The picture presets the seed picks among. */\n media: readonly AnimationPreset[]\n eases: readonly AnimationEase[]\n /** Inclusive millisecond ranges. */\n duration: readonly [number, number]\n stagger: readonly [number, number]\n hero: readonly [number, number]\n}\n\n/** Each mood's range. The seed picks one character from it. */\nexport const AI_SITE_MOTION_MOODS: Readonly<Record<AiSiteMotionMood, AiSiteMotionRange>> = {\n calm: {\n presets: ['fade'],\n media: ['fade'],\n eases: ['gentle-end', 'gentle-both'],\n duration: [650, 800],\n stagger: [100, 120],\n hero: [130, 160],\n },\n bold: {\n presets: ['slide-up'],\n media: ['zoom-in'],\n eases: ['overshoot'],\n duration: [450, 600],\n stagger: [60, 80],\n hero: [80, 100],\n },\n gallery: {\n presets: ['fade', 'slide-up'],\n media: ['zoom-in'],\n eases: ['smooth'],\n duration: [600, 750],\n stagger: [80, 110],\n hero: [110, 130],\n },\n standard: {\n presets: ['slide-up'],\n media: ['slide-up', 'fade'],\n eases: ['smooth'],\n duration: [500, 700],\n stagger: [70, 100],\n hero: [100, 130],\n },\n}\n\n/** Each site kind's mood; a kind not listed is standard. */\nexport const AI_SITE_MOTION_KINDS: Readonly<Record<string, AiSiteMotionMood>> = {\n professional: 'calm',\n wellness: 'calm',\n yoga: 'calm',\n beauty: 'calm',\n blog: 'calm',\n studio: 'bold',\n fitness: 'bold',\n music: 'bold',\n events: 'bold',\n portfolio: 'gallery',\n photography: 'gallery',\n}\n\n/** The mood a kind of site moves in. */\nexport function aiSiteMotionMood(kind: string | null | undefined): AiSiteMotionMood {\n return (kind && AI_SITE_MOTION_KINDS[kind]) || 'standard'\n}\n\n/**\n * A small deterministic generator over a seed (mulberry32), mixed with its\n * own constant so a site's motion does not move in step with its layout.\n */\nfunction random(seed: number): () => number {\n let state = (seed ^ 0x6a09e667) >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\nconst pick = <T>(next: () => number, list: readonly T[]): T => list[Math.floor(next() * list.length) % list.length] as T\n/** A value in an inclusive range, on a step: 10 ms for a duration, 5 ms for a gap. */\nconst within = (next: () => number, [low, high]: readonly [number, number], step: number): number =>\n low + Math.floor(next() * (Math.floor((high - low) / step) + 1)) * step\n\n/** The motion character of a site: its kind's mood, turned by its seed. */\nexport function aiSiteMotion(input: { kind?: string | null; seed: number }): ElementMotionCharacter {\n const range = AI_SITE_MOTION_MOODS[aiSiteMotionMood(input.kind)]\n const next = random(input.seed >>> 0)\n return {\n preset: pick(next, range.presets),\n mediaPreset: pick(next, range.media),\n ease: pick(next, range.eases),\n durationMs: within(next, range.duration, 10),\n staggerStepMs: within(next, range.stagger, 5),\n heroStepMs: within(next, range.hero, 5),\n }\n}\n\n/**\n * A page the AI built, with its site's entrance motion written onto it —\n * or the same node map, when there was nothing left to move.\n */\nexport function aiWithSiteMotion<T extends Record<string, any>>(\n nodes: T,\n site: { kind?: string | null; seed: number },\n page: { rootId?: string; sectionIds?: readonly string[] } = {},\n): T {\n return applyElementMotion(nodes, {\n character: aiSiteMotion(site),\n ...(page.rootId ? { rootId: page.rootId } : {}),\n ...(page.sectionIds?.length ? { sectionIds: page.sectionIds } : {}),\n })\n}\n"],"names":["applyElementMotion","AI_SITE_MOTION_MOODS","calm","presets","media","eases","duration","stagger","hero","bold","gallery","standard","AI_SITE_MOTION_KINDS","professional","wellness","yoga","beauty","blog","studio","fitness","music","events","portfolio","photography","aiSiteMotionMood","kind","random","seed","state","t","Math","imul","pick","next","list","floor","length","within","low","high","step","aiSiteMotion","input","range","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","aiWithSiteMotion","nodes","site","page","character","rootId","sectionIds"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,kBAAkB,QAEb,wCAAuC;AAuC9C,6DAA6D,GAC7D,OAAO,MAAMC,uBAA8E;IACzFC,MAAM;QACJC,SAAS;YAAC;SAAO;QACjBC,OAAO;YAAC;SAAO;QACfC,OAAO;YAAC;YAAc;SAAc;QACpCC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAK;SAAI;QACnBC,MAAM;YAAC;YAAK;SAAI;IAClB;IACAC,MAAM;QACJN,SAAS;YAAC;SAAW;QACrBC,OAAO;YAAC;SAAU;QAClBC,OAAO;YAAC;SAAY;QACpBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAG;QACjBC,MAAM;YAAC;YAAI;SAAI;IACjB;IACAE,SAAS;QACPP,SAAS;YAAC;YAAQ;SAAW;QAC7BC,OAAO;YAAC;SAAU;QAClBC,OAAO;YAAC;SAAS;QACjBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAI;QAClBC,MAAM;YAAC;YAAK;SAAI;IAClB;IACAG,UAAU;QACRR,SAAS;YAAC;SAAW;QACrBC,OAAO;YAAC;YAAY;SAAO;QAC3BC,OAAO;YAAC;SAAS;QACjBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAI;QAClBC,MAAM;YAAC;YAAK;SAAI;IAClB;AACF,EAAC;AAED,0DAA0D,GAC1D,OAAO,MAAMI,uBAAmE;IAC9EC,cAAc;IACdC,UAAU;IACVC,MAAM;IACNC,QAAQ;IACRC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,OAAO;IACPC,QAAQ;IACRC,WAAW;IACXC,aAAa;AACf,EAAC;AAED,sCAAsC,GACtC,OAAO,SAASC,iBAAiBC,IAA+B;IAC9D,OAAO,AAACA,QAAQb,oBAAoB,CAACa,KAAK,IAAK;AACjD;AAEA;;;CAGC,GACD,SAASC,OAAOC,IAAY;IAC1B,IAAIC,QAAQ,AAACD,CAAAA,OAAO,UAAS,MAAO;IACpC,OAAO;QACLC,QAAQ,AAACA,QAAQ,eAAgB;QACjC,IAAIC,IAAID;QACRC,IAAIC,KAAKC,IAAI,CAACF,IAAKA,MAAM,IAAKA,IAAI;QAClCA,KAAKA,IAAIC,KAAKC,IAAI,CAACF,IAAKA,MAAM,GAAIA,IAAI;QACtC,OAAO,AAAC,CAAA,AAACA,CAAAA,IAAKA,MAAM,EAAE,MAAO,CAAA,IAAK;IACpC;AACF;AAEA,MAAMG,OAAO,CAAIC,MAAoBC,OAA0BA,IAAI,CAACJ,KAAKK,KAAK,CAACF,SAASC,KAAKE,MAAM,IAAIF,KAAKE,MAAM,CAAC;AACnH,oFAAoF,GACpF,MAAMC,SAAS,CAACJ,MAAoB,CAACK,KAAKC,KAAgC,EAAEC,OAC1EF,MAAMR,KAAKK,KAAK,CAACF,SAAUH,CAAAA,KAAKK,KAAK,CAAC,AAACI,CAAAA,OAAOD,GAAE,IAAKE,QAAQ,CAAA,KAAMA;AAErE,yEAAyE,GACzE,OAAO,SAASC,aAAaC,KAA6C;IACxE,MAAMC,QAAQ1C,oBAAoB,CAACuB,iBAAiBkB,MAAMjB,IAAI,EAAE;IAChE,MAAMQ,OAAOP,OAAOgB,MAAMf,IAAI,KAAK;IACnC,OAAO;QACLiB,QAAQZ,KAAKC,MAAMU,MAAMxC,OAAO;QAChC0C,aAAab,KAAKC,MAAMU,MAAMvC,KAAK;QACnC0C,MAAMd,KAAKC,MAAMU,MAAMtC,KAAK;QAC5B0C,YAAYV,OAAOJ,MAAMU,MAAMrC,QAAQ,EAAE;QACzC0C,eAAeX,OAAOJ,MAAMU,MAAMpC,OAAO,EAAE;QAC3C0C,YAAYZ,OAAOJ,MAAMU,MAAMnC,IAAI,EAAE;IACvC;AACF;AAEA;;;CAGC,GACD,OAAO,SAAS0C,iBACdC,KAAQ,EACRC,IAA4C,EAC5CC,OAA4D,CAAC,CAAC;QAKxDA;IAHN,OAAOrD,mBAAmBmD,OAAO;QAC/BG,WAAWb,aAAaW;OACpBC,KAAKE,MAAM,GAAG;QAAEA,QAAQF,KAAKE,MAAM;IAAC,IAAI,CAAC,GACzCF,EAAAA,mBAAAA,KAAKG,UAAU,qBAAfH,iBAAiBjB,MAAM,IAAG;QAAEoB,YAAYH,KAAKG,UAAU;IAAC,IAAI,CAAC;AAErE"}
@@ -29,16 +29,26 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
29
29
  * 1. A stock photo searched for by the slot's own description and copied into
30
30
  * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the
31
31
  * deployment has a stock-photo key.
32
- * 2. Otherwise, or when the search finds nothing fitting, one of the STARTER
33
- * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at
34
- * `/_static/starter/` by the tenant and the console alike, credited in the
35
- * CREDITS file beside them). They are the platform's own files, so a
36
- * visitor's browser asks no other host for them.
32
+ * 2. Where the library cannot be asked at all — no stock-photo key, the
33
+ * library down, refusing or out of time before it answered — one of the
34
+ * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,
35
+ * served at `/_static/starter/` by the tenant and the console alike,
36
+ * credited in the CREDITS file beside them). They are the platform's own
37
+ * files, so a visitor's browser asks no other host for them, and a
38
+ * starter reads better than no picture when there is no library.
37
39
  *
38
- * A slot keeps the alt text the model wrote unless the photo filling it
39
- * brings its own words: the model wrote it before any photo was found, so a
40
- * stock photo that is not what it says is described by what it is
41
- * (`aiStockPhotoAlt`).
40
+ * Where the library answered and nothing in it fits a slot
41
+ * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo
42
+ * treatment — its frame with its icon, or its section's own band behind a
43
+ * cover — and never a starter: the beta.257 Copper Kettle Catering start
44
+ * (2026-10-15) opened a barbecue caterer's home page on the starters'
45
+ * pottery workbench (AGL-3831). An empty picture draws nothing on the
46
+ * published page, and the Besigner shows the owner where to choose one.
47
+ *
48
+ * A slot keeps the alt text the model wrote only when it describes the photo
49
+ * placed: a stock photo that is not what it says is described by what it is
50
+ * (`aiStockPhotoAlt`), and a starter photo always by its own words, since
51
+ * the model never wrote for it (AGL-3831).
42
52
  */
43
53
  /** What a picture slot is for, read from where it sits and what it says. */
44
54
  export type AiLayoutPictureRole = 'hero' | 'about' | 'gallery';
@@ -93,34 +103,47 @@ export declare function aiLayoutPictureSlots(nodes: NodesMap, rootId: string, se
93
103
  ids: readonly string[];
94
104
  names: readonly string[];
95
105
  }): AiLayoutPictureSlot[];
96
- /** The starter photos, by name. */
106
+ /** The starter photos, by name, each with the words that describe it. */
97
107
  export declare const AI_LAYOUT_STARTER_PHOTOS: {
98
108
  readonly hero: {
99
109
  readonly src: "/_static/starter/hero-team.jpg";
100
110
  readonly width: 1600;
101
111
  readonly height: 1067;
112
+ readonly alt: "A small team working together around a table in a bright office";
102
113
  };
103
114
  readonly about: {
104
115
  readonly src: "/_static/starter/about-owner.jpg";
105
116
  readonly width: 1600;
106
117
  readonly height: 900;
118
+ readonly alt: "A smiling business owner standing behind the counter of her coffee shop";
107
119
  };
108
120
  readonly desk: {
109
121
  readonly src: "/_static/starter/gallery-desk.jpg";
110
122
  readonly width: 1600;
111
123
  readonly height: 1068;
124
+ readonly alt: "A tidy desk with a laptop, a lamp and fresh flowers";
112
125
  };
113
126
  readonly plants: {
114
127
  readonly src: "/_static/starter/gallery-plants.jpg";
115
128
  readonly width: 1200;
116
129
  readonly height: 900;
130
+ readonly alt: "Houseplants on a sunny windowsill";
117
131
  };
118
132
  readonly craft: {
119
133
  readonly src: "/_static/starter/gallery-craft.jpg";
120
134
  readonly width: 1600;
121
135
  readonly height: 1065;
136
+ readonly alt: "Hands working on a craft project at a workbench";
122
137
  };
123
138
  };
139
+ /**
140
+ * A source's answer for a slot the library answered and nothing in it fits
141
+ * (AGL-3831): the slot keeps its no-photo treatment rather than take a
142
+ * starter. Its source is empty, so a caller reading `src` to fill something
143
+ * that must hold a photo (a product's, a record's) falls back as it would on
144
+ * `null`.
145
+ */
146
+ export declare const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto>;
124
147
  /** A small stable number from a seed, so the same job always picks the same photos. */
125
148
  export declare function aiLayoutSeedNumber(seed: string): number;
126
149
  /**
@@ -148,15 +171,19 @@ export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlo
148
171
  export declare function aiLayoutPlacePictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>): NodesMap;
149
172
  /**
150
173
  * A source of photos found for the slots themselves — a stock search copied
151
- * into the site's library — answering one photo or `null` per slot, in order.
152
- * It may throw; a slot it leaves unfilled takes a starter photo.
174
+ * into the site's library — answering per slot, in order: a photo; or
175
+ * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,
176
+ * which leaves the slot without a photo; or `null` where the library could
177
+ * not be asked, which takes a starter photo. It may throw, and then every
178
+ * slot takes a starter.
153
179
  */
154
180
  export type AiLayoutPictureSource = (slots: readonly AiLayoutPictureSlot[]) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>;
155
181
  /**
156
182
  * Fills a stored page's empty picture slots (AGL-3660): from `source` where
157
- * one is given and it answers, else from the starter photos. Never throws,
158
- * and never leaves a page worse than it came: a picture is decoration, and a
159
- * job never fails over one.
183
+ * one is given and it answers; a slot it answers with nothing fitting stays
184
+ * without a photo; every other slot takes a starter photo, described by its
185
+ * own words (AGL-3831). Never throws, and never leaves a page worse than it
186
+ * came: a picture is decoration, and a job never fails over one.
160
187
  */
161
188
  export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
162
189
  rootId: string;
@@ -165,4 +192,6 @@ export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
165
192
  /** A per-job word, so two sites do not open with the same photo. */
166
193
  seed: string;
167
194
  source?: AiLayoutPictureSource | null;
195
+ /** The site's language (AGL-3750); a starter photo's English words are not its own. */
196
+ language?: string;
168
197
  }): Promise<NodesMap>;
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { DEFAULT_SITE_IMAGES } from "@aglyn/aglyn/app-utils/default-site";
18
+ import { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from "./ai-layout-labels.js";
18
19
  /** Words that say a picture shows people, or a section is about them. */ const PEOPLE = /\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;
19
20
  /**
20
21
  * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.
@@ -109,7 +110,18 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
109
110
  visit(rootId, null, -1);
110
111
  return slots;
111
112
  }
112
- /** The starter photos, by name. */ export const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES;
113
+ /** The starter photos, by name, each with the words that describe it. */ export const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES;
114
+ /**
115
+ * A source's answer for a slot the library answered and nothing in it fits
116
+ * (AGL-3831): the slot keeps its no-photo treatment rather than take a
117
+ * starter. Its source is empty, so a caller reading `src` to fill something
118
+ * that must hold a photo (a product's, a record's) falls back as it would on
119
+ * `null`.
120
+ */ export const AI_LAYOUT_NO_PHOTO = Object.freeze({
121
+ src: '',
122
+ width: 0,
123
+ height: 0
124
+ });
113
125
  /**
114
126
  * The starter photos each role draws from, best first. A hero draws from the
115
127
  * wide photos that read as a page's opening, an about picture leads with the
@@ -205,7 +217,8 @@ function rotate(list, by) {
205
217
  return {
206
218
  src: photo.src,
207
219
  width: photo.width,
208
- height: photo.height
220
+ height: photo.height,
221
+ alt: photo.alt
209
222
  };
210
223
  });
211
224
  }
@@ -254,9 +267,10 @@ function rotate(list, by) {
254
267
  }
255
268
  /**
256
269
  * Fills a stored page's empty picture slots (AGL-3660): from `source` where
257
- * one is given and it answers, else from the starter photos. Never throws,
258
- * and never leaves a page worse than it came: a picture is decoration, and a
259
- * job never fails over one.
270
+ * one is given and it answers; a slot it answers with nothing fitting stays
271
+ * without a photo; every other slot takes a starter photo, described by its
272
+ * own words (AGL-3831). Never throws, and never leaves a page worse than it
273
+ * came: a picture is decoration, and a job never fails over one.
260
274
  */ export async function aiResolveLayoutPictures(nodes, input) {
261
275
  try {
262
276
  const slots = aiLayoutPictureSlots(nodes, input.rootId, {
@@ -275,10 +289,17 @@ function rotate(list, by) {
275
289
  found = [];
276
290
  }
277
291
  }
278
- const starters = aiLayoutStarterPhotos(slots, input.seed);
292
+ // The starters' words are English; a site in another language says it
293
+ // shows an illustrative photo, true of any photo, as a stock photo's does.
294
+ const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language);
295
+ const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo)=>otherLanguage ? _extends({}, photo, {
296
+ alt: aiLayoutLabel('illustrativePhoto', input.language)
297
+ }) : photo);
279
298
  return aiLayoutPlacePictures(nodes, slots, slots.map((_slot, index)=>{
280
- var _found_index;
281
- return (_found_index = found[index]) != null ? _found_index : starters[index];
299
+ var _starters_index;
300
+ const answer = found[index];
301
+ if (answer) return answer.src ? answer : null;
302
+ return (_starters_index = starters[index]) != null ? _starters_index : null;
282
303
  }));
283
304
  } catch (error) {
284
305
  console.error('ai layout pictures: the page keeps its empty picture slots', {
@@ -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'\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. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * A slot keeps the alt text the model wrote unless the photo filling it\n * brings its own words: the model wrote it before any photo was found, so a\n * stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`).\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. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\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 }\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 one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\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, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * 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 },\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 const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\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","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","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","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","width","height","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}
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":"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"}
@@ -529,6 +529,20 @@ const OPEN = [
529
529
  result.notes.push(`Its submissions arrive in the Inbox only: the dataset “${dep.label}” could not be created.`);
530
530
  continue;
531
531
  }
532
+ // An item that links pages this build makes (a menu's entries, AGL-3728)
533
+ // is built with the ones it made and without the ones it could not
534
+ // (AGL-3832), and skipped only when it made none of them.
535
+ const madeAPage = unit.deps.some((one)=>{
536
+ var _bySlot_get;
537
+ const made = rows.get(one);
538
+ return !!((_bySlot_get = bySlot.get(one)) == null ? void 0 : _bySlot_get.screen) && !!made && aiBuildItemDelivered(made);
539
+ });
540
+ if (!unit.screen && dep.screen && madeAPage) {
541
+ result.degradedBy.push(slot);
542
+ result.dropped.add(dep.label.toLowerCase());
543
+ result.notes.push(`Built without the ${noun} “${dep.label}”: it could not be created.`);
544
+ continue;
545
+ }
532
546
  if (!unit.screen && degrade === 'omit') {
533
547
  result.skip = `Not built: the ${noun} “${dep.label}” it needs could not be created.`;
534
548
  result.degradedBy.push(slot);