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

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 (199) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +25 -0
  3. package/src/lib/activity/ai-activity-actions.js +85 -4
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-brief-dialog.component.js +46 -2
  6. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  7. package/src/lib/components/ai-campaign-create.component.js +1 -1
  8. package/src/lib/components/ai-campaign-create.component.js.map +1 -1
  9. package/src/lib/components/ai-credits-card.component.js +3 -3
  10. package/src/lib/components/ai-credits-card.component.js.map +1 -1
  11. package/src/lib/components/ai-crm-email-draft.component.js +1 -1
  12. package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
  13. package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
  14. package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
  15. package/src/lib/components/ai-crm-record-card.component.js +1 -1
  16. package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
  17. package/src/lib/components/ai-describe-automation.component.js +4 -2
  18. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  19. package/src/lib/components/ai-describe-org-automation.component.js +1 -1
  20. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
  21. package/src/lib/components/ai-experiment-cards.component.js +2 -2
  22. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  23. package/src/lib/components/ai-explain-automation.component.js +1 -1
  24. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  25. package/src/lib/components/ai-insight-card.component.js +3 -1
  26. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  27. package/src/lib/components/ai-job-links.d.ts +6 -0
  28. package/src/lib/components/ai-job-links.js +9 -0
  29. package/src/lib/components/ai-job-links.js.map +1 -1
  30. package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
  31. package/src/lib/components/ai-jobs-list-page.component.js +15 -2
  32. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  33. package/src/lib/components/ai-logic.component.js +5 -3
  34. package/src/lib/components/ai-logic.component.js.map +1 -1
  35. package/src/lib/components/ai-media-create.component.js +88 -16
  36. package/src/lib/components/ai-media-create.component.js.map +1 -1
  37. package/src/lib/components/ai-model-selector.component.js +8 -0
  38. package/src/lib/components/ai-model-selector.component.js.map +1 -1
  39. package/src/lib/components/ai-overlay-cards.component.js +2 -2
  40. package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
  41. package/src/lib/components/ai-product-copy-card.component.js +2 -1
  42. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  43. package/src/lib/components/ai-products-hub-card.component.js +2 -1
  44. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  45. package/src/lib/components/ai-revise-automation.component.js +1 -1
  46. package/src/lib/components/ai-revise-automation.component.js.map +1 -1
  47. package/src/lib/components/ai-seo-fields-card.component.js +7 -3
  48. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  49. package/src/lib/components/ai-site-batch-card.component.js +7 -4
  50. package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
  51. package/src/lib/components/ai-site-build-page.component.js +48 -6
  52. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  53. package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
  54. package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
  55. package/src/lib/components/ai-site-start-card.component.js +152 -36
  56. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  57. package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/ai-upsell-dialog.component.js +17 -3
  60. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  61. package/src/lib/components/ai-usage-strip.component.js +13 -2
  62. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  63. package/src/lib/components/assist-build-card.component.js +20 -3
  64. package/src/lib/components/assist-build-card.component.js.map +1 -1
  65. package/src/lib/components/assist-jobs-drawer.component.js +7 -1
  66. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  67. package/src/lib/components/billing-ai-allotments.component.js +2 -2
  68. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  69. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  70. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  71. package/src/lib/components/billing-assist-overage-card.component.js +2 -2
  72. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  73. package/src/lib/components/host-ai-allotments.component.js +2 -2
  74. package/src/lib/components/host-ai-allotments.component.js.map +1 -1
  75. package/src/lib/components/member-ai-allotment-card.component.js +2 -2
  76. package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
  77. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  78. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  79. package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
  80. package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
  81. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  82. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  83. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  84. package/src/lib/jobs/ai-job-admission.d.ts +2 -1
  85. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  86. package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
  87. package/src/lib/jobs/ai-job-email-step.js +116 -5
  88. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  89. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  90. package/src/lib/jobs/ai-job-layout-language.js +6 -1
  91. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  92. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +14 -0
  93. package/src/lib/jobs/ai-job-layout-site-pages.js +43 -11
  94. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  95. package/src/lib/jobs/ai-job-layout-step.js +3 -2
  96. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  97. package/src/lib/jobs/ai-job-page-language.d.ts +26 -1
  98. package/src/lib/jobs/ai-job-page-language.js +75 -13
  99. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  100. package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
  101. package/src/lib/jobs/ai-job-page-step.js +32 -7
  102. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
  104. package/src/lib/jobs/ai-job-plan-step.js +106 -14
  105. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-site-content.d.ts +81 -13
  107. package/src/lib/jobs/ai-job-site-content.js +210 -43
  108. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  109. package/src/lib/jobs/ai-job-site-step.d.ts +19 -0
  110. package/src/lib/jobs/ai-job-site-step.js +152 -51
  111. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  112. package/src/lib/jobs/ai-jobs.d.ts +8 -4
  113. package/src/lib/jobs/ai-jobs.js +19 -2
  114. package/src/lib/jobs/ai-jobs.js.map +1 -1
  115. package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
  116. package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
  117. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
  118. package/src/lib/jobs/ai-site-publish.d.ts +11 -0
  119. package/src/lib/jobs/ai-site-publish.js +31 -1
  120. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  121. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
  122. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
  123. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
  124. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
  125. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
  126. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
  127. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
  128. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
  129. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
  130. package/src/lib/layout-language/ai-layout-compiler.d.ts +37 -0
  131. package/src/lib/layout-language/ai-layout-compiler.js +1003 -19
  132. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  133. package/src/lib/layout-language/ai-layout-design.d.ts +114 -0
  134. package/src/lib/layout-language/ai-layout-design.js +209 -0
  135. package/src/lib/layout-language/ai-layout-design.js.map +1 -0
  136. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  137. package/src/lib/layout-language/ai-layout-frame.js +33 -8
  138. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  139. package/src/lib/layout-language/ai-layout-gaps.js +18 -2
  140. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  141. package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
  142. package/src/lib/layout-language/ai-layout-language.js +15 -1
  143. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  144. package/src/lib/layout-language/ai-layout-links.d.ts +36 -0
  145. package/src/lib/layout-language/ai-layout-links.js +26 -2
  146. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  147. package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
  148. package/src/lib/layout-language/ai-layout-listings.js +187 -0
  149. package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
  150. package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
  151. package/src/lib/layout-language/ai-layout-pictures.js +80 -17
  152. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  153. package/src/lib/model/ai-job-notice.d.ts +9 -1
  154. package/src/lib/model/ai-job-notice.js +23 -0
  155. package/src/lib/model/ai-job-notice.js.map +1 -1
  156. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  157. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  158. package/src/lib/model/ai-site-build-progress.d.ts +12 -2
  159. package/src/lib/model/ai-site-build-progress.js +104 -15
  160. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  161. package/src/lib/model/ai-site-job.d.ts +236 -24
  162. package/src/lib/model/ai-site-job.js +269 -22
  163. package/src/lib/model/ai-site-job.js.map +1 -1
  164. package/src/lib/model/ai-site-kinds.d.ts +30 -8
  165. package/src/lib/model/ai-site-kinds.js +735 -116
  166. package/src/lib/model/ai-site-kinds.js.map +1 -1
  167. package/src/lib/model/ai-site-look.d.ts +21 -0
  168. package/src/lib/model/ai-site-look.js +63 -14
  169. package/src/lib/model/ai-site-look.js.map +1 -1
  170. package/src/lib/model/ai-site-start-seo.d.ts +30 -0
  171. package/src/lib/model/ai-site-start-seo.js +132 -11
  172. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-start.d.ts +2 -2
  174. package/src/lib/model/ai-site-start.js +3 -3
  175. package/src/lib/model/ai-site-start.js.map +1 -1
  176. package/src/lib/plugin.js +15 -3
  177. package/src/lib/plugin.js.map +1 -1
  178. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
  179. package/src/lib/runtime/ai-doctrine-validators.js +227 -4
  180. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  181. package/src/lib/runtime/ai-doctrine.js +5 -3
  182. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  183. package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
  184. package/src/lib/runtime/ai-palette.generated.js +31 -5
  185. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  186. package/src/lib/server/ai-admin-conversations.d.ts +9 -0
  187. package/src/lib/server/ai-admin-conversations.js +151 -9
  188. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  189. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  190. package/src/lib/server/ai-jobs-route.js +15 -3
  191. package/src/lib/server/ai-jobs-route.js.map +1 -1
  192. package/src/lib/server/assist-edit.js +5 -2
  193. package/src/lib/server/assist-edit.js.map +1 -1
  194. package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
  195. package/src/lib/usage/free-ai-credits-left.js +62 -0
  196. package/src/lib/usage/free-ai-credits-left.js.map +1 -0
  197. package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
  198. package/src/lib/usage/staff-org-ai-conversations.js +45 -0
  199. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest';
18
+ import { type StockPhoto, type StockPhotoOrientation, type StockPhotoProvider, type StockPhotoSearchRequest } from '@aglyn/aglyn/plugin-manager/stock-photo-provider';
19
+ import { type AiLayoutPictureSlot, type AiLayoutPictureSource } from '../layout-language/ai-layout-pictures';
20
+ /**
21
+ * STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).
22
+ *
23
+ * The source `aiResolveLayoutPictures` asks before it falls back to the
24
+ * starter photos. It names no library: it asks core for the deployment's
25
+ * stock photo provider (`core.stock-photos`) and for the media library's
26
+ * server door (`core.media-ingest`), and builds no source at all when either
27
+ * is missing, so a deployment without a key behaves exactly as before.
28
+ *
29
+ * For each slot, in document order:
30
+ *
31
+ * 1. **Search.** Plain words: the kind of business (the head of the site's
32
+ * business type, "yoga studio" from "a family-owned yoga studio in
33
+ * Austin") for the hero; the picture's own subject (the content words of
34
+ * the alt text the model wrote, else its section's name) with the business
35
+ * for the rest, and an about picture asks for people. The orientation and
36
+ * a minimum size come from the slot's frame. The provider caches every
37
+ * search for a day and keeps its own rate limit.
38
+ * 2. **Pick.** Among the first hits not already on the page, by the job's
39
+ * seed and the slot, so two sites built from the same words rarely open
40
+ * with the same photo while one job always picks the same.
41
+ * 3. **Keep.** A photo the site's library already holds (by its source key)
42
+ * is reused; otherwise its bytes are downloaded from the library and
43
+ * stored in the site's own media library as the member who started the
44
+ * job, credited to its photographer. The page names the site's asset,
45
+ * never the library's address.
46
+ *
47
+ * A slot that finds nothing, any error, a refusal from the library (its
48
+ * storage band, say) or the time running out leaves the slot to a starter
49
+ * photo. Nothing here throws, and none of it is an AI model call: photos cost
50
+ * no AI credits.
51
+ */
52
+ /** Wall clock the whole page's photos may take, inside the step's own signal. */
53
+ export declare const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20000;
54
+ /** Searches one page may ask, cached ones included. */
55
+ export declare const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8;
56
+ /** The hits a pick is made among. */
57
+ export declare const AI_LAYOUT_STOCK_PICK_WINDOW = 8;
58
+ /** The largest photo copied: inside one direct upload. */
59
+ export declare const AI_LAYOUT_STOCK_MAX_BYTES: number;
60
+ /**
61
+ * The kind of business in a few words: the first clause of the business
62
+ * type, before a place or an audience, its last three content words (the
63
+ * head of an English noun phrase is at its end).
64
+ */
65
+ export declare function aiStockBusinessWords(businessType: string): string;
66
+ /** A picture's subject in a few words: its alt text's first content words, else its section's name. */
67
+ export declare function aiStockSubjectWords(alt: string, sectionName: string): string;
68
+ /** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */
69
+ export declare function aiStockOrientation(aspect: number): StockPhotoOrientation;
70
+ /** The searches a slot tries, in order, most specific first, each distinct. */
71
+ export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, words: {
72
+ business: string;
73
+ subject: string;
74
+ }): StockPhotoSearchRequest[];
75
+ /** The pick among a search's hits: not on the page yet, among the first few, by the seed. */
76
+ export declare function aiStockPick(photos: readonly StockPhoto[], used: ReadonlySet<string>, seed: string): StockPhoto | null;
77
+ export interface AiLayoutStockPhotoSourceInput {
78
+ hostId: string;
79
+ /** The member the photos are stored as: the job's creator. */
80
+ uid: string;
81
+ /** The job's seed, as the starter photos turn by. */
82
+ seed: string;
83
+ /** The site's business type, else the job's brief. */
84
+ business: string;
85
+ /** The page's section names, by plan index. */
86
+ sectionNames: readonly string[];
87
+ signal?: AbortSignal;
88
+ }
89
+ export interface AiLayoutStockPhotoDeps {
90
+ provider?: () => StockPhotoProvider | null;
91
+ ingest?: () => PluginMediaIngest | null;
92
+ budgetMs?: number;
93
+ }
94
+ /**
95
+ * The source for one page, or `null` when this deployment has no stock
96
+ * photo library or no media door, which leaves every slot to the starters.
97
+ */
98
+ export declare function aiLayoutStockPhotoSource(input: AiLayoutStockPhotoSourceInput, deps?: AiLayoutStockPhotoDeps): AiLayoutPictureSource | null;
@@ -0,0 +1,248 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
18
+ import { STOCK_PHOTO_QUERY_MAX_CHARS, stockPhotoProvider, stockPhotoSourceKey } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
19
+ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
20
+ /**
21
+ * STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).
22
+ *
23
+ * The source `aiResolveLayoutPictures` asks before it falls back to the
24
+ * starter photos. It names no library: it asks core for the deployment's
25
+ * stock photo provider (`core.stock-photos`) and for the media library's
26
+ * server door (`core.media-ingest`), and builds no source at all when either
27
+ * is missing, so a deployment without a key behaves exactly as before.
28
+ *
29
+ * For each slot, in document order:
30
+ *
31
+ * 1. **Search.** Plain words: the kind of business (the head of the site's
32
+ * business type, "yoga studio" from "a family-owned yoga studio in
33
+ * Austin") for the hero; the picture's own subject (the content words of
34
+ * the alt text the model wrote, else its section's name) with the business
35
+ * for the rest, and an about picture asks for people. The orientation and
36
+ * a minimum size come from the slot's frame. The provider caches every
37
+ * search for a day and keeps its own rate limit.
38
+ * 2. **Pick.** Among the first hits not already on the page, by the job's
39
+ * seed and the slot, so two sites built from the same words rarely open
40
+ * with the same photo while one job always picks the same.
41
+ * 3. **Keep.** A photo the site's library already holds (by its source key)
42
+ * is reused; otherwise its bytes are downloaded from the library and
43
+ * stored in the site's own media library as the member who started the
44
+ * job, credited to its photographer. The page names the site's asset,
45
+ * never the library's address.
46
+ *
47
+ * A slot that finds nothing, any error, a refusal from the library (its
48
+ * storage band, say) or the time running out leaves the slot to a starter
49
+ * photo. Nothing here throws, and none of it is an AI model call: photos cost
50
+ * no AI credits.
51
+ */ /** Wall clock the whole page's photos may take, inside the step's own signal. */ export const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20000;
52
+ /** Searches one page may ask, cached ones included. */ export const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8;
53
+ /** The hits a pick is made among. */ export const AI_LAYOUT_STOCK_PICK_WINDOW = 8;
54
+ /** The largest photo copied: inside one direct upload. */ export const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024;
55
+ /** Words that carry no subject. */ const STOPWORDS = new Set(('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 ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company shop services service site website page photo picture image showing shows show ' + 'someone people person one two three some many several').split(' '));
56
+ /** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
57
+ return text.toLowerCase().replace(/https?:\/\/\S+|\S+@\S+|\+?\d[\d\s().-]{5,}\d/g, ' ').replace(/[^a-zÀ-ɏ\s-]/g, ' ').split(/\s+/).map((word)=>word.replace(/^-+|-+$/g, '')).filter((word)=>word.length > 1 && !STOPWORDS.has(word));
58
+ }
59
+ /**
60
+ * The kind of business in a few words: the first clause of the business
61
+ * type, before a place or an audience, its last three content words (the
62
+ * head of an English noun phrase is at its end).
63
+ */ export function aiStockBusinessWords(businessType) {
64
+ const clause = businessType.toLowerCase().split(/[,;:.()]|\s[-–—]\s|\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\b/)[0];
65
+ const words = contentWords(clause != null ? clause : '');
66
+ return words.slice(-3).join(' ');
67
+ }
68
+ /** A picture's subject in a few words: its alt text's first content words, else its section's name. */ export function aiStockSubjectWords(alt, sectionName) {
69
+ const words = contentWords(alt);
70
+ if (words.length) return words.slice(0, 3).join(' ');
71
+ return contentWords(sectionName).slice(0, 2).join(' ');
72
+ }
73
+ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim();
74
+ /** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */ export function aiStockOrientation(aspect) {
75
+ if (aspect > 1.15) return 'horizontal';
76
+ if (aspect < 0.87) return 'vertical';
77
+ return 'any';
78
+ }
79
+ /** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, words) {
80
+ const orientation = aiStockOrientation(slot.aspect);
81
+ const size = slot.role === 'hero' ? {
82
+ minWidth: 1600
83
+ } : orientation === 'vertical' ? {
84
+ minHeight: 900
85
+ } : {
86
+ minWidth: 900
87
+ };
88
+ const queries = slot.role === 'hero' ? [
89
+ words.business,
90
+ `${words.business} ${words.subject}`
91
+ ] : slot.role === 'about' ? [
92
+ `${words.business} ${words.subject}`,
93
+ words.business
94
+ ] : [
95
+ `${words.business} ${words.subject}`,
96
+ words.subject,
97
+ words.business
98
+ ];
99
+ const seen = new Set();
100
+ const searches = [];
101
+ for (const raw of queries){
102
+ const query = clip(raw);
103
+ if (!query || seen.has(query)) continue;
104
+ seen.add(query);
105
+ searches.push(_extends({
106
+ query,
107
+ orientation
108
+ }, size, slot.role === 'about' ? {
109
+ people: true
110
+ } : {}));
111
+ }
112
+ return searches;
113
+ }
114
+ /** The pick among a search's hits: not on the page yet, among the first few, by the seed. */ export function aiStockPick(photos, used, seed) {
115
+ var _fresh_;
116
+ const fresh = photos.filter((photo)=>!used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW);
117
+ if (!fresh.length) return null;
118
+ return (_fresh_ = fresh[aiLayoutSeedNumber(seed) % fresh.length]) != null ? _fresh_ : null;
119
+ }
120
+ const EXTENSIONS = {
121
+ 'image/jpeg': 'jpg',
122
+ 'image/png': 'png',
123
+ 'image/webp': 'webp'
124
+ };
125
+ /**
126
+ * The source for one page, or `null` when this deployment has no stock
127
+ * photo library or no media door, which leaves every slot to the starters.
128
+ */ export function aiLayoutStockPhotoSource(input, deps = {}) {
129
+ var _deps_provider, _deps_ingest;
130
+ const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
131
+ const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
132
+ if (!provider || !ingest || !input.hostId || !input.uid) return null;
133
+ const business = aiStockBusinessWords(input.business);
134
+ return async (slots)=>{
135
+ var _deps_budgetMs;
136
+ const timeout = AbortSignal.timeout((_deps_budgetMs = deps.budgetMs) != null ? _deps_budgetMs : AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS);
137
+ const signal = input.signal ? AbortSignal.any([
138
+ input.signal,
139
+ timeout
140
+ ]) : timeout;
141
+ const used = new Set();
142
+ let searches = 0;
143
+ let storing = true;
144
+ const photos = [];
145
+ for (const [index, slot] of slots.entries()){
146
+ var _input_sectionNames_slot_sectionIndex;
147
+ if (signal.aborted) {
148
+ photos.push(null);
149
+ continue;
150
+ }
151
+ const subject = aiStockSubjectWords(slot.alt, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '');
152
+ let placed = null;
153
+ for (const request of aiStockSearchesFor(slot, {
154
+ business,
155
+ subject
156
+ })){
157
+ if (placed || signal.aborted || searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) break;
158
+ searches += 1;
159
+ let found;
160
+ try {
161
+ var _ref;
162
+ var _this;
163
+ found = (_ref = (_this = await provider.search(request, {
164
+ signal
165
+ })) == null ? void 0 : _this.photos) != null ? _ref : [];
166
+ } catch (unused) {
167
+ found = [];
168
+ }
169
+ const photo = aiStockPick(found, used, `${input.seed}:${index}:${request.query}`);
170
+ if (!photo) continue;
171
+ const sourceKey = stockPhotoSourceKey(photo);
172
+ used.add(sourceKey);
173
+ try {
174
+ var _EXTENSIONS_bytes_contentType;
175
+ const kept = await ingest.findStockPhoto({
176
+ hostId: input.hostId,
177
+ sourceKey
178
+ });
179
+ if (kept) {
180
+ var _kept_width, _kept_height;
181
+ placed = {
182
+ src: kept.src,
183
+ width: (_kept_width = kept.width) != null ? _kept_width : photo.width,
184
+ height: (_kept_height = kept.height) != null ? _kept_height : photo.height
185
+ };
186
+ break;
187
+ }
188
+ // A refusal to store (the storage band, a lockdown) holds for the
189
+ // whole page, so the rest of it reuses what it can and copies nothing.
190
+ if (!storing) continue;
191
+ const bytes = await provider.download(photo, {
192
+ maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
193
+ signal
194
+ });
195
+ if (!bytes || signal.aborted) continue;
196
+ const credit = provider.credit(photo);
197
+ const stored = await ingest.ingest({
198
+ hostId: input.hostId,
199
+ uid: input.uid,
200
+ fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
201
+ contentType: bytes.contentType,
202
+ bytes: bytes.bytes,
203
+ alt: slot.alt,
204
+ description: credit.text,
205
+ stockPhoto: _extends({
206
+ key: sourceKey,
207
+ provider: photo.provider,
208
+ providerLabel: credit.providerLabel,
209
+ id: photo.id,
210
+ pageUrl: photo.pageUrl,
211
+ photographer: photo.photographer
212
+ }, photo.photographerUrl ? {
213
+ photographerUrl: photo.photographerUrl
214
+ } : {}, {
215
+ license: credit.license,
216
+ licenseUrl: credit.licenseUrl,
217
+ attributionRequired: credit.attributionRequired,
218
+ query: request.query
219
+ })
220
+ });
221
+ if (stored.ok) {
222
+ var _stored_width, _stored_height;
223
+ placed = {
224
+ src: stored.src,
225
+ width: (_stored_width = stored.width) != null ? _stored_width : photo.width,
226
+ height: (_stored_height = stored.height) != null ? _stored_height : photo.height
227
+ };
228
+ } else {
229
+ storing = false;
230
+ const refusal = stored;
231
+ console.warn('ai layout pictures: the media library refused a stock photo', {
232
+ status: refusal.status,
233
+ reason: refusal.reason
234
+ });
235
+ }
236
+ } catch (error) {
237
+ console.warn('ai layout pictures: a stock photo could not be kept', {
238
+ error: String(error)
239
+ });
240
+ }
241
+ }
242
+ photos.push(placed);
243
+ }
244
+ return photos;
245
+ };
246
+ }
247
+
248
+ //# sourceMappingURL=ai-layout-stock-photos.js.map
@@ -0,0 +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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"}
@@ -80,6 +80,13 @@ interface NavEntry {
80
80
  screenId: string;
81
81
  label: string;
82
82
  }
83
+ /**
84
+ * The blog paths a start's layout links and must not publish (AGL-3660): its
85
+ * header, phone menu and footer link the blog by path while the posts part is
86
+ * still owed, so a start whose posts part then failed or was skipped drops
87
+ * every address the blog could have taken. None while the blog was written.
88
+ */
89
+ export declare function aiSiteUnwrittenBlogHrefs(blogUnwritten: boolean | undefined): string[];
83
90
  /**
84
91
  * The layout's nodes with its links to the retired starter home pointed at
85
92
  * the new home, and a link for each proposed entry the header does not
@@ -91,6 +98,8 @@ export declare function aiLayoutWithNavigation(nodes: NodesMap, input: {
91
98
  entries: readonly NavEntry[];
92
99
  retiredHomeId?: string | null;
93
100
  homeId?: string | null;
101
+ /** Paths whose links come out: a blog the start linked and never wrote (AGL-3660). */
102
+ droppedHrefs?: readonly string[];
94
103
  }): NodesMap | null;
95
104
  /**
96
105
  * Publishes the pages a guided site start built. Never throws for a page; a
@@ -101,5 +110,7 @@ export declare function aiPublishGuidedSite(firestore: Firestore, input: {
101
110
  job: Pick<AiJob, '$id' | 'orgId' | 'hostId'>;
102
111
  outputs: readonly AiJobOutput[];
103
112
  now: Date;
113
+ /** The start owed its blog's posts and wrote none (AGL-3660): its links to the blog come out. */
114
+ blogUnwritten?: boolean;
104
115
  }, deps?: AiSitePublishDeps): Promise<AiJobSitePublish>;
105
116
  export {};
@@ -22,6 +22,7 @@ import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/sto
22
22
  import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
23
23
  import { sendGa4SitePublished } from "@aglyn/tenant-data-admin/server/ga4-measurement-protocol";
24
24
  import { FieldValue } from "firebase-admin/firestore";
25
+ import { AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
25
26
  /**
26
27
  * Whether a build puts its pages live (AGL-3616): only where its request
27
28
  * asked to (`publish`, set by Assist's proposal) AND the member ticked the
@@ -67,6 +68,14 @@ function navEntriesOf(outputs) {
67
68
  }
68
69
  return entries;
69
70
  }
71
+ /**
72
+ * The blog paths a start's layout links and must not publish (AGL-3660): its
73
+ * header, phone menu and footer link the blog by path while the posts part is
74
+ * still owed, so a start whose posts part then failed or was skipped drops
75
+ * every address the blog could have taken. None while the blog was written.
76
+ */ export function aiSiteUnwrittenBlogHrefs(blogUnwritten) {
77
+ return blogUnwritten ? AI_SITE_BLOG_SLUGS.map((slug)=>`/${slug}`) : [];
78
+ }
70
79
  /**
71
80
  * The layout's nodes with its links to the retired starter home pointed at
72
81
  * the new home, and a link for each proposed entry the header does not
@@ -74,6 +83,7 @@ function navEntriesOf(outputs) {
74
83
  * `null` when nothing changes, or when the layout has no toolbar to hold an
75
84
  * entry and no link to repoint.
76
85
  */ export function aiLayoutWithNavigation(nodes, input) {
86
+ var _input_droppedHrefs;
77
87
  const map = {};
78
88
  for (const [id, node] of Object.entries(nodes)){
79
89
  map[id] = _extends({}, node, node.props ? {
@@ -85,6 +95,25 @@ function navEntriesOf(outputs) {
85
95
  } : {});
86
96
  }
87
97
  let changed = false;
98
+ // The header, the phone menu and the footer link a blog by its path before
99
+ // its posts are written (AGL-3660); a start whose posts were not written
100
+ // publishes none of those links to a page that does not exist.
101
+ const dropped = new Set((_input_droppedHrefs = input.droppedHrefs) != null ? _input_droppedHrefs : []);
102
+ if (dropped.size) {
103
+ const gone = new Set(Object.entries(map).filter(([, node])=>{
104
+ var _ref;
105
+ var _node_props;
106
+ return node.componentId === 'muiScreenLink' && dropped.has(String((_ref = (_node_props = node.props) == null ? void 0 : _node_props['href']) != null ? _ref : ''));
107
+ }).map(([id])=>id));
108
+ if (gone.size) {
109
+ for (const id of gone)delete map[id];
110
+ for (const node of Object.values(map)){
111
+ var _node_nodes;
112
+ if ((_node_nodes = node.nodes) == null ? void 0 : _node_nodes.some((child)=>gone.has(child))) node.nodes = node.nodes.filter((child)=>!gone.has(child));
113
+ }
114
+ changed = true;
115
+ }
116
+ }
88
117
  const links = Object.values(map).filter((node)=>node.componentId === 'muiScreenLink');
89
118
  if (input.retiredHomeId && input.homeId) {
90
119
  for (const link of links){
@@ -223,6 +252,7 @@ function navEntriesOf(outputs) {
223
252
  hostId,
224
253
  pageIds: accepted.map((page)=>page.id),
225
254
  entries: navEntriesOf(pages).filter((entry)=>entries[entry.screenId]),
255
+ droppedHrefs: aiSiteUnwrittenBlogHrefs(input.blogUnwritten),
226
256
  retiredHomeId: releases,
227
257
  homeId: (_ref = home == null ? void 0 : home.id) != null ? _ref : null,
228
258
  jobId: job.$id,
@@ -337,7 +367,7 @@ function navEntriesOf(outputs) {
337
367
  * their navigation entries and the home link moved off a retired starter;
338
368
  * `null` when there is nothing to change or no one layout to change.
339
369
  */ async function aiNavigationLayoutWrite(firestore, input) {
340
- if (!input.entries.length && !input.retiredHomeId) return null;
370
+ if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length) return null;
341
371
  const hostRef = firestore.collection('hosts').doc(input.hostId);
342
372
  const layoutIds = new Set();
343
373
  for (const id of input.pageIds){