@aglyn/plugins-ai 1.0.0-beta.269 → 1.0.0-beta.271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +1 -1
- package/src/lib/activity/ai-activity-actions.js +1 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/components/ai-credits-card.component.d.ts +8 -6
- package/src/lib/components/ai-credits-card.component.js +39 -22
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-job-input-form.component.d.ts +36 -0
- package/src/lib/components/ai-job-input-form.component.js +100 -0
- package/src/lib/components/ai-job-input-form.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +4 -0
- package/src/lib/components/ai-job-requests.js +4 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.d.ts +6 -0
- package/src/lib/components/ai-products-hub-card.component.js +21 -6
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +3 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +99 -31
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +43 -2
- package/src/lib/components/ai-site-start-steps.component.js +651 -15
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-start-tracking.d.ts +10 -0
- package/src/lib/components/ai-site-start-tracking.js +14 -0
- package/src/lib/components/ai-site-start-tracking.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.js +26 -22
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/ai-store-finish-card.component.d.ts +14 -3
- package/src/lib/components/ai-store-finish-card.component.js +67 -9
- package/src/lib/components/ai-store-finish-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +7 -5
- package/src/lib/components/ai-upsell-dialog.component.js +8 -6
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +14 -3
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +9 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +8 -4
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +4 -7
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/declarations.d.ts +2 -2
- package/src/lib/declarations.js +9 -5
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +3 -1
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.d.ts +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.js +14 -3
- package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-build-step.js +13 -5
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +24 -1
- package/src/lib/jobs/ai-job-form-step.js +93 -7
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +35 -11
- package/src/lib/jobs/ai-job-page-language.js +68 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-page-step.js +40 -17
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +25 -2
- package/src/lib/jobs/ai-job-plan-step.js +103 -29
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.js +4 -3
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +27 -0
- package/src/lib/jobs/ai-job-site-datasets.js +140 -14
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-look.js +18 -5
- package/src/lib/jobs/ai-job-site-look.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +16 -1
- package/src/lib/jobs/ai-job-site-step.js +95 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +8 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +14 -3
- package/src/lib/jobs/ai-job-tracking-step.js +28 -5
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -1
- package/src/lib/jobs/ai-jobs.js +63 -8
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.d.ts +57 -0
- package/src/lib/jobs/ai-layout-owner-photos.js +110 -0
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +62 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +523 -72
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-inspiration-signals.d.ts +98 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js +585 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js.map +1 -0
- package/src/lib/jobs/ai-site-inspiration.d.ts +157 -0
- package/src/lib/jobs/ai-site-inspiration.js +346 -0
- package/src/lib/jobs/ai-site-inspiration.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +26 -1
- package/src/lib/jobs/ai-site-publish.js +162 -13
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +16 -0
- package/src/lib/jobs/ai-theme-brand-inputs.js +18 -4
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.d.ts +97 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js +137 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.d.ts +38 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js +103 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3942-recordings.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3942-recordings.js +291 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3942-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.d.ts +25 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js +174 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +1 -1
- package/src/lib/layout-language/ai-layout-compiler.js +438 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-contact.d.ts +57 -0
- package/src/lib/layout-language/ai-layout-contact.js +137 -0
- package/src/lib/layout-language/ai-layout-contact.js.map +1 -0
- package/src/lib/layout-language/ai-layout-copy.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-copy.js +34 -2
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +65 -8
- package/src/lib/layout-language/ai-layout-design.js +411 -97
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.js +23 -0
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-labels.js +12 -6
- package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +3 -2
- package/src/lib/layout-language/ai-layout-language.js +4 -3
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +13 -0
- package/src/lib/layout-language/ai-layout-links.js +30 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +49 -6
- package/src/lib/layout-language/ai-layout-pictures.js +145 -22
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +16 -1
- package/src/lib/model/ai-build-job.js +40 -0
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-job-input.d.ts +33 -0
- package/src/lib/model/ai-job-input.js +59 -0
- package/src/lib/model/ai-job-input.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +4 -0
- package/src/lib/model/ai-job-notice.js +11 -1
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +40 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-details.d.ts +122 -0
- package/src/lib/model/ai-site-details.js +300 -0
- package/src/lib/model/ai-site-details.js.map +1 -0
- package/src/lib/model/ai-site-entity.d.ts +181 -0
- package/src/lib/model/ai-site-entity.js +390 -0
- package/src/lib/model/ai-site-entity.js.map +1 -0
- package/src/lib/model/ai-site-inspiration.d.ts +72 -0
- package/src/lib/model/ai-site-inspiration.js +109 -0
- package/src/lib/model/ai-site-inspiration.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +13 -1
- package/src/lib/model/ai-site-job.js +15 -2
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +109 -21
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +10 -0
- package/src/lib/model/ai-site-look.js +24 -0
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -5
- package/src/lib/model/ai-site-start-seo.js +25 -7
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +9 -3
- package/src/lib/model/ai-site-start-steps.js +67 -5
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +68 -4
- package/src/lib/model/ai-site-start.js +166 -13
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-upsell-copy.js +4 -1
- package/src/lib/model/ai-upsell-copy.js.map +1 -1
- package/src/lib/native-nav.d.ts +26 -0
- package/src/lib/native-nav.js +58 -0
- package/src/lib/native-nav.js.map +1 -0
- package/src/lib/plan-entitlements.d.ts +4 -3
- package/src/lib/plan-entitlements.js +3 -8
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +8 -1
- package/src/lib/runtime/ai-dataset-generation.js +12 -2
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +26 -33
- package/src/lib/runtime/ai-doctrine-validators.js +54 -44
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +7 -0
- package/src/lib/runtime/ai-node-tree.js +9 -1
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +1 -1
- package/src/lib/server/ai-create-route.js +3 -4
- package/src/lib/server/ai-create-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +27 -3
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +7 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/usage/assist-credits.d.ts +4 -4
- package/src/lib/usage/assist-credits.js +4 -4
- package/src/lib/usage/assist-credits.js.map +1 -1
- package/src/lib/usage/staff-org-ai-view.js +6 -3
- package/src/lib/usage/staff-org-ai-view.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +4 -2
- package/src/lib/usage/staff-org-ai.js +7 -7
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage/staff-org-usage-ai.js +2 -2
- package/src/lib/usage/staff-org-usage-ai.js.map +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import type { AiSiteContact } from '../model/ai-site-details';
|
|
17
18
|
import type { AiLayoutListing } from './ai-layout-listings';
|
|
18
19
|
/**
|
|
19
20
|
* Where a link the layout language names goes (AGL-3660), resolved by code to
|
|
@@ -70,6 +71,13 @@ export interface AiLayoutTargets {
|
|
|
70
71
|
* `ai-layout-listings.ts`). Absent or empty, a page shows only its words.
|
|
71
72
|
*/
|
|
72
73
|
listings?: readonly AiLayoutListing[];
|
|
74
|
+
/**
|
|
75
|
+
* The business's phone number and email, where the job was given them
|
|
76
|
+
* (AGL-3979): a link to `phone` calls the number and one to `email` writes
|
|
77
|
+
* to the address, and the footer and the page that places the site's form
|
|
78
|
+
* show them. Absent, neither link goes anywhere.
|
|
79
|
+
*/
|
|
80
|
+
contact?: AiSiteContact | null;
|
|
73
81
|
}
|
|
74
82
|
/** Where a link resolved to. */
|
|
75
83
|
export type AiLayoutDestination = {
|
|
@@ -92,6 +100,11 @@ export type AiLayoutDestination = {
|
|
|
92
100
|
href: string;
|
|
93
101
|
standsInFor?: string;
|
|
94
102
|
label?: string;
|
|
103
|
+
}
|
|
104
|
+
/** The business's phone (`tel:`) or email (`mailto:`) the job was given (AGL-3979). */
|
|
105
|
+
| {
|
|
106
|
+
kind: 'contact';
|
|
107
|
+
href: string;
|
|
95
108
|
};
|
|
96
109
|
/**
|
|
97
110
|
* A link's words for where it now goes (AGL-3676): a button that named a
|
|
@@ -126,6 +126,10 @@ function resolveNamed(to, label, scope, targets) {
|
|
|
126
126
|
href: to
|
|
127
127
|
} : null;
|
|
128
128
|
}
|
|
129
|
+
// A call or an email goes only to the number and the address the job was
|
|
130
|
+
// given (AGL-3979), however the link spells them.
|
|
131
|
+
const contact = contactDestination(lower, targets.contact);
|
|
132
|
+
if (contact !== undefined) return contact;
|
|
129
133
|
const section = lower.startsWith('#') ? to.slice(1) : lower.startsWith('section:') ? to.slice(8) : null;
|
|
130
134
|
if (section !== null) {
|
|
131
135
|
const number = Number(section.trim());
|
|
@@ -138,5 +142,31 @@ function resolveNamed(to, label, scope, targets) {
|
|
|
138
142
|
const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets);
|
|
139
143
|
return pageDestination(page, label, targets);
|
|
140
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Where a link to the business's phone or email goes (AGL-3979): `phone`,
|
|
147
|
+
* `call` or a `tel:` link with the given number's digits is the number;
|
|
148
|
+
* `email` or a `mailto:` link to the given address is the address. `null` is
|
|
149
|
+
* such a link with nothing given to go to, which is left out; `undefined` is
|
|
150
|
+
* a link that names neither.
|
|
151
|
+
*/ function contactDestination(lower, contact) {
|
|
152
|
+
const digits = (value)=>value.replace(/\D/g, '');
|
|
153
|
+
if (lower === 'phone' || lower === 'call' || lower === 'tel' || lower.startsWith('tel:')) {
|
|
154
|
+
if (!(contact == null ? void 0 : contact.phoneHref)) return null;
|
|
155
|
+
if (lower.startsWith('tel:') && digits(lower) !== digits(contact.phoneHref)) return null;
|
|
156
|
+
return {
|
|
157
|
+
kind: 'contact',
|
|
158
|
+
href: contact.phoneHref
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (lower === 'email' || lower === 'mail' || lower.startsWith('mailto:')) {
|
|
162
|
+
if (!(contact == null ? void 0 : contact.emailHref)) return null;
|
|
163
|
+
if (lower.startsWith('mailto:') && lower.slice(7).split('?')[0] !== contact.email.toLowerCase()) return null;
|
|
164
|
+
return {
|
|
165
|
+
kind: 'contact',
|
|
166
|
+
href: contact.emailHref
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
141
171
|
|
|
142
172
|
//# sourceMappingURL=ai-layout-links.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiAsksToGetInTouch } from '../runtime/ai-doctrine-validators'\nimport { aiPageLinkSection } from '../runtime/ai-page-links'\nimport type { AiLayoutListing } from './ai-layout-listings'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n /**\n * A destination on the site that is no page of its own, linked by its path:\n * the blog a guided start writes its posts into (AGL-3660), whose `id` is\n * no screen. Absent for every page.\n */\n href?: string\n /**\n * The title of a planned page merged into this destination (AGL-3676): a\n * page that stood in for the blog, kept only so links to it go to the blog.\n * A link to it is named for the blog, not for the page it replaced.\n */\n standsInFor?: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n /**\n * The kinds of record the site keeps that its pages show — its catalog,\n * its blog — and the sections that place each (AGL-3676,\n * `ai-layout-listings.ts`). Absent or empty, a page shows only its words.\n */\n listings?: readonly AiLayoutListing[]\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n /**\n * A path on this site that is no page of its own, such as the blog\n * (AGL-3660); reached through a planned page merged into it, the title\n * that page had and the name it now goes by (AGL-3676).\n */\n | { kind: 'path'; href: string; standsInFor?: string; label?: string }\n\n/**\n * A link's words for where it now goes (AGL-3676): a button that named a\n * page merged into the blog — \"Read the articles\" — names the blog instead,\n * \"Read the blog\". Words that name neither are kept.\n */\nexport function aiLayoutRenamedLabel(label: string, destination: AiLayoutDestination): string {\n if (destination.kind !== 'path' || !destination.standsInFor || !destination.label) return label\n const name = destination.label.toLowerCase()\n const words = wordsOf(destination.standsInFor)\n if (!words.length) return label\n // The page's own name, or its words alone, singular or plural, whole: a\n // letter in any script continues a word (AGL-3791). `\\b` reads \"ú\" or \"í\"\n // as no letter, so it never matched \"Menú\" and matched \"art\" inside\n // \"artículos\".\n const stems = words.map((word) => word.replace(/s$/, ''))\n const pattern = new RegExp(\n `(?<![\\\\p{L}\\\\p{M}\\\\p{N}])(?:${stems.map((stem) => stem.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('\\\\s+')})s?(?![\\\\p{L}\\\\p{M}\\\\p{N}])`,\n 'giu',\n )\n const renamed = label.replace(pattern, (found) => (found[0] === found[0].toUpperCase() ? destination.label as string : name))\n return renamed\n}\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers: rule 10's own list (AGL-3729). */\nconst CONTACT_WORDS = { test: aiAsksToGetInTouch }\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n // A page merged into the blog is still named by the title it had (AGL-3676).\n targets.pages.find((page) => page.standsInFor && wordsOf(page.standsInFor).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n // A link home says so, in English or by the home page's own title: a\n // Spanish site's home is \"Inicio\" (AGL-3760).\n const saysHome = HOME_WORDS.test(label) || (wordsOf(label).join(' ') !== '' && wordsOf(label).join(' ') === wordsOf(page.label).join(' '))\n if (targets.homeIds.includes(page.id) && !saysHome && !isHomeSlug(page.slug)) return null\n if (isHomeSlug(page.slug) && !saysHome) return null\n if (page.href) return { kind: 'path', href: page.href, ...(page.standsInFor ? { standsInFor: page.standsInFor, label: page.label } : {}) }\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n"],"names":["aiAsksToGetInTouch","aiPageLinkSection","aiLayoutRenamedLabel","label","destination","kind","standsInFor","name","toLowerCase","words","wordsOf","length","stems","map","word","replace","pattern","RegExp","stem","join","renamed","found","toUpperCase","text","split","filter","Boolean","slugOf","slug","HOME_WORDS","CONTACT_WORDS","test","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","saysHome","homeIds","includes","isHomeSlug","href","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","every","has","forms","some","facts","section","slice","number","Number","isInteger","sections"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiAsksToGetInTouch } from '../runtime/ai-doctrine-validators'\nimport { aiPageLinkSection } from '../runtime/ai-page-links'\nimport type { AiSiteContact } from '../model/ai-site-details'\nimport type { AiLayoutListing } from './ai-layout-listings'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n /**\n * A destination on the site that is no page of its own, linked by its path:\n * the blog a guided start writes its posts into (AGL-3660), whose `id` is\n * no screen. Absent for every page.\n */\n href?: string\n /**\n * The title of a planned page merged into this destination (AGL-3676): a\n * page that stood in for the blog, kept only so links to it go to the blog.\n * A link to it is named for the blog, not for the page it replaced.\n */\n standsInFor?: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n /**\n * The kinds of record the site keeps that its pages show — its catalog,\n * its blog — and the sections that place each (AGL-3676,\n * `ai-layout-listings.ts`). Absent or empty, a page shows only its words.\n */\n listings?: readonly AiLayoutListing[]\n /**\n * The business's phone number and email, where the job was given them\n * (AGL-3979): a link to `phone` calls the number and one to `email` writes\n * to the address, and the footer and the page that places the site's form\n * show them. Absent, neither link goes anywhere.\n */\n contact?: AiSiteContact | null\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n /**\n * A path on this site that is no page of its own, such as the blog\n * (AGL-3660); reached through a planned page merged into it, the title\n * that page had and the name it now goes by (AGL-3676).\n */\n | { kind: 'path'; href: string; standsInFor?: string; label?: string }\n /** The business's phone (`tel:`) or email (`mailto:`) the job was given (AGL-3979). */\n | { kind: 'contact'; href: string }\n\n/**\n * A link's words for where it now goes (AGL-3676): a button that named a\n * page merged into the blog — \"Read the articles\" — names the blog instead,\n * \"Read the blog\". Words that name neither are kept.\n */\nexport function aiLayoutRenamedLabel(label: string, destination: AiLayoutDestination): string {\n if (destination.kind !== 'path' || !destination.standsInFor || !destination.label) return label\n const name = destination.label.toLowerCase()\n const words = wordsOf(destination.standsInFor)\n if (!words.length) return label\n // The page's own name, or its words alone, singular or plural, whole: a\n // letter in any script continues a word (AGL-3791). `\\b` reads \"ú\" or \"í\"\n // as no letter, so it never matched \"Menú\" and matched \"art\" inside\n // \"artículos\".\n const stems = words.map((word) => word.replace(/s$/, ''))\n const pattern = new RegExp(\n `(?<![\\\\p{L}\\\\p{M}\\\\p{N}])(?:${stems.map((stem) => stem.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('\\\\s+')})s?(?![\\\\p{L}\\\\p{M}\\\\p{N}])`,\n 'giu',\n )\n const renamed = label.replace(pattern, (found) => (found[0] === found[0].toUpperCase() ? destination.label as string : name))\n return renamed\n}\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers: rule 10's own list (AGL-3729). */\nconst CONTACT_WORDS = { test: aiAsksToGetInTouch }\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n // A page merged into the blog is still named by the title it had (AGL-3676).\n targets.pages.find((page) => page.standsInFor && wordsOf(page.standsInFor).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n // A link home says so, in English or by the home page's own title: a\n // Spanish site's home is \"Inicio\" (AGL-3760).\n const saysHome = HOME_WORDS.test(label) || (wordsOf(label).join(' ') !== '' && wordsOf(label).join(' ') === wordsOf(page.label).join(' '))\n if (targets.homeIds.includes(page.id) && !saysHome && !isHomeSlug(page.slug)) return null\n if (isHomeSlug(page.slug) && !saysHome) return null\n if (page.href) return { kind: 'path', href: page.href, ...(page.standsInFor ? { standsInFor: page.standsInFor, label: page.label } : {}) }\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n // A call or an email goes only to the number and the address the job was\n // given (AGL-3979), however the link spells them.\n const contact = contactDestination(lower, targets.contact)\n if (contact !== undefined) return contact\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n\n/**\n * Where a link to the business's phone or email goes (AGL-3979): `phone`,\n * `call` or a `tel:` link with the given number's digits is the number;\n * `email` or a `mailto:` link to the given address is the address. `null` is\n * such a link with nothing given to go to, which is left out; `undefined` is\n * a link that names neither.\n */\nfunction contactDestination(\n lower: string,\n contact: AiSiteContact | null | undefined,\n): AiLayoutDestination | null | undefined {\n const digits = (value: string) => value.replace(/\\D/g, '')\n if (lower === 'phone' || lower === 'call' || lower === 'tel' || lower.startsWith('tel:')) {\n if (!contact?.phoneHref) return null\n if (lower.startsWith('tel:') && digits(lower) !== digits(contact.phoneHref)) return null\n return { kind: 'contact', href: contact.phoneHref }\n }\n if (lower === 'email' || lower === 'mail' || lower.startsWith('mailto:')) {\n if (!contact?.emailHref) return null\n if (lower.startsWith('mailto:') && lower.slice(7).split('?')[0] !== contact.email.toLowerCase()) return null\n return { kind: 'contact', href: contact.emailHref }\n }\n return undefined\n}\n"],"names":["aiAsksToGetInTouch","aiPageLinkSection","aiLayoutRenamedLabel","label","destination","kind","standsInFor","name","toLowerCase","words","wordsOf","length","stems","map","word","replace","pattern","RegExp","stem","join","renamed","found","toUpperCase","text","split","filter","Boolean","slugOf","slug","HOME_WORDS","CONTACT_WORDS","test","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","saysHome","homeIds","includes","isHomeSlug","href","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","every","has","forms","some","facts","contactDestination","undefined","section","slice","number","Number","isInteger","sections","digits","phoneHref","emailHref","email"],"mappings":"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"}
|
|
@@ -24,13 +24,19 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
|
|
|
24
24
|
* stored page — it finds each empty slot, decides what kind of picture the
|
|
25
25
|
* slot is, and fills it with a photo the site can serve from its own origin.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Three sources fill a slot, in order:
|
|
28
28
|
*
|
|
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. Where the library
|
|
33
|
-
*
|
|
32
|
+
* 2. Where the library failed the slot — down, refusing, out of time, or its
|
|
33
|
+
* photo not copied — one of the photos the same job already placed on
|
|
34
|
+
* this site, on its other pages or its datasets' records
|
|
35
|
+
* ({@link aiLayoutJobPhotoPicks}, AGL-3988): each was found for the
|
|
36
|
+
* site's own world, so a wedding photographer's home page shows a wedding
|
|
37
|
+
* and never an office.
|
|
38
|
+
* 3. Where the job has no photo of its own — no stock-photo key, or the
|
|
39
|
+
* library failed it from the start — one of the
|
|
34
40
|
* STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,
|
|
35
41
|
* served at `/_static/starter/` by the tenant and the console alike,
|
|
36
42
|
* credited in the CREDITS file beside them). They are the platform's own
|
|
@@ -190,6 +196,14 @@ export declare function aiLayoutSeedNumber(seed: string): number;
|
|
|
190
196
|
* people; a gallery picture never takes it, even to avoid a repeat.
|
|
191
197
|
*/
|
|
192
198
|
export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlot[], seed: string): AiLayoutPicturePhoto[];
|
|
199
|
+
/**
|
|
200
|
+
* A neutral starter for each of a list's records (AGL-3988), where the site
|
|
201
|
+
* has a library and the job has no photo of its own: the two taken in turn
|
|
202
|
+
* from the seed, so no record shows the photo of the one beside it. A card
|
|
203
|
+
* of a listing cannot drop its frame as a page's picture does, so every
|
|
204
|
+
* record takes one. Pure.
|
|
205
|
+
*/
|
|
206
|
+
export declare function aiLayoutNeutralStarterRun(count: number, seed: string): AiLayoutPicturePhoto[];
|
|
193
207
|
/**
|
|
194
208
|
* A neutral starter for each slot a library could not fill, where the site
|
|
195
209
|
* has a library (AGL-3923): each at most once on a page, the hero served
|
|
@@ -198,6 +212,25 @@ export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlo
|
|
|
198
212
|
* none keeps the no-photo treatment.
|
|
199
213
|
*/
|
|
200
214
|
export declare function aiLayoutNeutralStarterPhotos(slots: readonly AiLayoutPictureSlot[], seed: string): Array<AiLayoutPicturePhoto | null>;
|
|
215
|
+
/**
|
|
216
|
+
* The job's own photos for the slots a library could not fill (AGL-3988):
|
|
217
|
+
* the photos THIS job already placed on this site — on its other pages, on
|
|
218
|
+
* its datasets' records, and on this page — which were each found for the
|
|
219
|
+
* site's own world, before any starter. The beta.270 Sugar Hollow
|
|
220
|
+
* Photography start (2026-10-19) opened a wedding photographer's home page
|
|
221
|
+
* on the starters' desk and an office team, and filed five wedding galleries
|
|
222
|
+
* under a workbench, a windowsill and the same office, while the library
|
|
223
|
+
* had already given the job three wedding and family photos.
|
|
224
|
+
*
|
|
225
|
+
* Each slot `need` names takes the one that fits it best: one this page does
|
|
226
|
+
* not show yet before one it does, a wide photo for a hero, the shape
|
|
227
|
+
* nearest its frame, and the most words shared with what the slot says it
|
|
228
|
+
* shows; never the photo of the slot beside it. A product's photo and a
|
|
229
|
+
* portrait of the business's people are of one named thing, which no other
|
|
230
|
+
* photo is, so they take none. Among equals the seed picks, so one job
|
|
231
|
+
* always picks the same. `null` for a slot given none. Pure.
|
|
232
|
+
*/
|
|
233
|
+
export declare function aiLayoutJobPhotoPicks(slots: readonly AiLayoutPictureSlot[], need: readonly boolean[], jobPhotos: readonly AiLayoutPicturePhoto[], placed: ReadonlyArray<AiLayoutPicturePhoto | null | undefined>, seed: string): Array<AiLayoutPicturePhoto | null>;
|
|
201
234
|
/**
|
|
202
235
|
* The page with each slot filled by its photo: the `image` given its source,
|
|
203
236
|
* its intrinsic size and how it loads (the hero eagerly, as the starter home
|
|
@@ -232,9 +265,13 @@ export type AiLayoutPictureSource = (slots: readonly AiLayoutPictureSlot[]) => P
|
|
|
232
265
|
/**
|
|
233
266
|
* Fills a stored page's empty picture slots (AGL-3660): from `source` where
|
|
234
267
|
* one is given and it answers; a slot it answers with nothing fitting stays
|
|
235
|
-
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures})
|
|
236
|
-
*
|
|
237
|
-
*
|
|
268
|
+
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures}). A
|
|
269
|
+
* slot it could not answer — the library down, out of time, or its photo not
|
|
270
|
+
* copied — takes one of the photos the job already placed on this site
|
|
271
|
+
* (`jobPhotos`, {@link aiLayoutJobPhotoPicks}, AGL-3988), and only where the
|
|
272
|
+
* job has none of its own, a starter photo described by its own words
|
|
273
|
+
* (AGL-3831). Never throws, and never leaves a page worse than it came: a
|
|
274
|
+
* picture is decoration, and a job never fails over one.
|
|
238
275
|
*/
|
|
239
276
|
export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
|
|
240
277
|
rootId: string;
|
|
@@ -243,6 +280,12 @@ export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
|
|
|
243
280
|
/** A per-job word, so two sites do not open with the same photo. */
|
|
244
281
|
seed: string;
|
|
245
282
|
source?: AiLayoutPictureSource | null;
|
|
283
|
+
/**
|
|
284
|
+
* The photos this job already placed on this site (AGL-3988): its other
|
|
285
|
+
* pages' and its datasets' records', each found for the site's world.
|
|
286
|
+
* Taken before any starter where `source` could not fill a slot.
|
|
287
|
+
*/
|
|
288
|
+
jobPhotos?: readonly AiLayoutPicturePhoto[];
|
|
246
289
|
/** The site's language (AGL-3750); a starter photo's English words are not its own. */
|
|
247
290
|
language?: string;
|
|
248
291
|
}): Promise<NodesMap>;
|
|
@@ -244,6 +244,26 @@ function rotate(list, by) {
|
|
|
244
244
|
'hero',
|
|
245
245
|
'desk'
|
|
246
246
|
];
|
|
247
|
+
/**
|
|
248
|
+
* A neutral starter for each of a list's records (AGL-3988), where the site
|
|
249
|
+
* has a library and the job has no photo of its own: the two taken in turn
|
|
250
|
+
* from the seed, so no record shows the photo of the one beside it. A card
|
|
251
|
+
* of a listing cannot drop its frame as a page's picture does, so every
|
|
252
|
+
* record takes one. Pure.
|
|
253
|
+
*/ export function aiLayoutNeutralStarterRun(count, seed) {
|
|
254
|
+
const pool = rotate(NEUTRAL_STARTERS, aiLayoutSeedNumber(seed));
|
|
255
|
+
return Array.from({
|
|
256
|
+
length: Math.max(0, count)
|
|
257
|
+
}, (_, index)=>{
|
|
258
|
+
const photo = AI_LAYOUT_STARTER_PHOTOS[pool[index % pool.length]];
|
|
259
|
+
return {
|
|
260
|
+
src: photo.src,
|
|
261
|
+
width: photo.width,
|
|
262
|
+
height: photo.height,
|
|
263
|
+
alt: photo.alt
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
247
267
|
/**
|
|
248
268
|
* A neutral starter for each slot a library could not fill, where the site
|
|
249
269
|
* has a library (AGL-3923): each at most once on a page, the hero served
|
|
@@ -275,6 +295,77 @@ function rotate(list, by) {
|
|
|
275
295
|
});
|
|
276
296
|
return photos;
|
|
277
297
|
}
|
|
298
|
+
/** A text's words a photo's own words may share: lowercase, four letters or more. */ const wordsOf = (text)=>new Set(text.toLowerCase().split(/[^a-zÀ-ɏ]+/).filter((word)=>word.length >= 4));
|
|
299
|
+
/**
|
|
300
|
+
* The job's own photos for the slots a library could not fill (AGL-3988):
|
|
301
|
+
* the photos THIS job already placed on this site — on its other pages, on
|
|
302
|
+
* its datasets' records, and on this page — which were each found for the
|
|
303
|
+
* site's own world, before any starter. The beta.270 Sugar Hollow
|
|
304
|
+
* Photography start (2026-10-19) opened a wedding photographer's home page
|
|
305
|
+
* on the starters' desk and an office team, and filed five wedding galleries
|
|
306
|
+
* under a workbench, a windowsill and the same office, while the library
|
|
307
|
+
* had already given the job three wedding and family photos.
|
|
308
|
+
*
|
|
309
|
+
* Each slot `need` names takes the one that fits it best: one this page does
|
|
310
|
+
* not show yet before one it does, a wide photo for a hero, the shape
|
|
311
|
+
* nearest its frame, and the most words shared with what the slot says it
|
|
312
|
+
* shows; never the photo of the slot beside it. A product's photo and a
|
|
313
|
+
* portrait of the business's people are of one named thing, which no other
|
|
314
|
+
* photo is, so they take none. Among equals the seed picks, so one job
|
|
315
|
+
* always picks the same. `null` for a slot given none. Pure.
|
|
316
|
+
*/ export function aiLayoutJobPhotoPicks(slots, need, jobPhotos, placed, seed) {
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
const pool = jobPhotos.filter((photo)=>{
|
|
319
|
+
if (!photo.src || !(photo.width > 0) || !(photo.height > 0) || seen.has(photo.src)) return false;
|
|
320
|
+
seen.add(photo.src);
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
const picks = slots.map(()=>null);
|
|
324
|
+
if (!pool.length) return picks;
|
|
325
|
+
const shown = slots.map((_slot, index)=>{
|
|
326
|
+
var _placed_index;
|
|
327
|
+
return ((_placed_index = placed[index]) == null ? void 0 : _placed_index.src) || '';
|
|
328
|
+
});
|
|
329
|
+
const order = [
|
|
330
|
+
'hero',
|
|
331
|
+
'about',
|
|
332
|
+
'gallery'
|
|
333
|
+
];
|
|
334
|
+
const served = slots.map((slot, index)=>({
|
|
335
|
+
slot,
|
|
336
|
+
index
|
|
337
|
+
})).filter(({ slot, index })=>need[index] && !slot.product && !slot.people).sort((a, b)=>order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index);
|
|
338
|
+
const turn = aiLayoutSeedNumber(seed);
|
|
339
|
+
for (const { slot, index } of served){
|
|
340
|
+
const beside = new Set([
|
|
341
|
+
shown[index - 1],
|
|
342
|
+
shown[index + 1]
|
|
343
|
+
].filter(Boolean));
|
|
344
|
+
const says = wordsOf(slot.alt);
|
|
345
|
+
let best = null;
|
|
346
|
+
for (const [at, photo] of pool.entries()){
|
|
347
|
+
var _photo_alt;
|
|
348
|
+
if (beside.has(photo.src)) continue;
|
|
349
|
+
const aspect = photo.width / photo.height;
|
|
350
|
+
const uses = shown.filter((src)=>src === photo.src).length;
|
|
351
|
+
const shared = [
|
|
352
|
+
...wordsOf((_photo_alt = photo.alt) != null ? _photo_alt : '')
|
|
353
|
+
].filter((word)=>says.has(word)).length;
|
|
354
|
+
const score = -100 * uses + (slot.role === 'hero' && aspect >= 1.3 ? 20 : 0) - 10 * Math.abs(Math.log(aspect / (slot.aspect > 0 ? slot.aspect : 1))) + 4 * shared;
|
|
355
|
+
// The seed turns the pool, so equals are taken in a per-job order.
|
|
356
|
+
const rank = (at - turn % pool.length + pool.length) % pool.length;
|
|
357
|
+
if (!best || score > best.score || score === best.score && rank < best.rank) best = {
|
|
358
|
+
photo,
|
|
359
|
+
score,
|
|
360
|
+
rank
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (!best) continue;
|
|
364
|
+
picks[index] = _extends({}, best.photo);
|
|
365
|
+
shown[index] = best.photo.src;
|
|
366
|
+
}
|
|
367
|
+
return picks;
|
|
368
|
+
}
|
|
278
369
|
/**
|
|
279
370
|
* The page with each slot filled by its photo: the `image` given its source,
|
|
280
371
|
* its intrinsic size and how it loads (the hero eagerly, as the starter home
|
|
@@ -290,7 +381,7 @@ function rotate(list, by) {
|
|
|
290
381
|
const lead = slots.findIndex((_, index)=>Boolean(photos[index] && next[slots[index].imageId]));
|
|
291
382
|
slots.forEach((slot, index)=>{
|
|
292
383
|
var _node_props;
|
|
293
|
-
var _photo_alt
|
|
384
|
+
var _photo_alt;
|
|
294
385
|
const photo = photos[index];
|
|
295
386
|
const node = next[slot.imageId];
|
|
296
387
|
if (!photo || !node) return;
|
|
@@ -298,14 +389,18 @@ function rotate(list, by) {
|
|
|
298
389
|
delete props['loading'];
|
|
299
390
|
// A photo placed is described by its words, never hidden from a screen
|
|
300
391
|
// reader: a stand-in was decorative only while it showed nothing (AGL-3856).
|
|
301
|
-
|
|
392
|
+
// One that brings no words of its own, such as an owner's upload with no
|
|
393
|
+
// alt text, keeps the slot's (AGL-3979): a picture a visitor sees always
|
|
394
|
+
// has a description.
|
|
395
|
+
const alt = ((_photo_alt = photo.alt) == null ? void 0 : _photo_alt.trim()) || (typeof props['alt'] === 'string' ? props['alt'].trim() : '');
|
|
396
|
+
if (alt) delete props['decorative'];
|
|
302
397
|
next[slot.imageId] = _extends({}, node, {
|
|
303
398
|
props: _extends({}, props, {
|
|
304
399
|
src: photo.src,
|
|
305
400
|
intrinsicWidth: photo.width,
|
|
306
401
|
intrinsicHeight: photo.height
|
|
307
|
-
},
|
|
308
|
-
alt
|
|
402
|
+
}, alt ? {
|
|
403
|
+
alt
|
|
309
404
|
} : {}, index === lead && slot.role === 'hero' ? {
|
|
310
405
|
loading: 'eager'
|
|
311
406
|
} : {})
|
|
@@ -404,11 +499,16 @@ function rotate(list, by) {
|
|
|
404
499
|
/**
|
|
405
500
|
* Fills a stored page's empty picture slots (AGL-3660): from `source` where
|
|
406
501
|
* one is given and it answers; a slot it answers with nothing fitting stays
|
|
407
|
-
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures})
|
|
408
|
-
*
|
|
409
|
-
*
|
|
502
|
+
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures}). A
|
|
503
|
+
* slot it could not answer — the library down, out of time, or its photo not
|
|
504
|
+
* copied — takes one of the photos the job already placed on this site
|
|
505
|
+
* (`jobPhotos`, {@link aiLayoutJobPhotoPicks}, AGL-3988), and only where the
|
|
506
|
+
* job has none of its own, a starter photo described by its own words
|
|
507
|
+
* (AGL-3831). Never throws, and never leaves a page worse than it came: a
|
|
508
|
+
* picture is decoration, and a job never fails over one.
|
|
410
509
|
*/ export async function aiResolveLayoutPictures(nodes, input) {
|
|
411
510
|
try {
|
|
511
|
+
var _input_jobPhotos;
|
|
412
512
|
const slots = aiLayoutPictureSlots(nodes, input.rootId, {
|
|
413
513
|
ids: input.sectionIds,
|
|
414
514
|
names: input.sectionNames
|
|
@@ -419,7 +519,7 @@ function rotate(list, by) {
|
|
|
419
519
|
try {
|
|
420
520
|
found = await input.source(slots);
|
|
421
521
|
} catch (error) {
|
|
422
|
-
console.warn('ai layout pictures: the stock source failed;
|
|
522
|
+
console.warn('ai layout pictures: the stock source failed; the job’s own photos or starters fill the page', {
|
|
423
523
|
error
|
|
424
524
|
});
|
|
425
525
|
found = [];
|
|
@@ -431,28 +531,51 @@ function rotate(list, by) {
|
|
|
431
531
|
const said = (photo)=>otherLanguage ? _extends({}, photo, {
|
|
432
532
|
alt: aiLayoutLabel('illustrativePhoto', input.language)
|
|
433
533
|
}) : photo;
|
|
434
|
-
|
|
435
|
-
// beta.267 Brightvale start, whose library refused its copies, showed a
|
|
436
|
-
// pottery bench and a coffee shop's owner as a marketer's pictures.
|
|
437
|
-
const starters = (input.source ? aiLayoutNeutralStarterPhotos(slots, input.seed) : aiLayoutStarterPhotos(slots, input.seed)).map((photo)=>photo ? said(photo) : null);
|
|
534
|
+
/** The slots the source could not answer: no library, or it failed them. */ const unanswered = slots.map((_slot, index)=>!found[index]);
|
|
438
535
|
const photos = slots.map((_slot, index)=>{
|
|
439
|
-
var _starters_index;
|
|
440
536
|
const answer = found[index];
|
|
441
|
-
|
|
442
|
-
return (_starters_index = starters[index]) != null ? _starters_index : null;
|
|
537
|
+
return (answer == null ? void 0 : answer.src) ? answer : null;
|
|
443
538
|
});
|
|
539
|
+
// With a library, the job's own photos come before any starter (AGL-3988),
|
|
540
|
+
// and a starter is only ever a neutral one (AGL-3923): the beta.267
|
|
541
|
+
// Brightvale start, whose library refused its copies, showed a pottery
|
|
542
|
+
// bench and a coffee shop's owner as a marketer's pictures.
|
|
543
|
+
const own = input.source ? ((_input_jobPhotos = input.jobPhotos) != null ? _input_jobPhotos : []).filter((photo)=>photo.src && photo.width > 0 && photo.height > 0) : [];
|
|
544
|
+
if (own.length) {
|
|
545
|
+
aiLayoutJobPhotoPicks(slots, unanswered, own, photos, input.seed).forEach((pick, index)=>{
|
|
546
|
+
if (pick && unanswered[index]) photos[index] = pick;
|
|
547
|
+
});
|
|
548
|
+
} else {
|
|
549
|
+
const starters = input.source ? aiLayoutNeutralStarterPhotos(slots, input.seed) : aiLayoutStarterPhotos(slots, input.seed);
|
|
550
|
+
starters.forEach((photo, index)=>{
|
|
551
|
+
if (photo && unanswered[index]) photos[index] = said(photo);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
444
554
|
// The page floor (AGL-3923; Zach, 2026-10-12: a starter photo before no
|
|
445
555
|
// photo at all): a page the library answered that would show no picture
|
|
446
|
-
// keeps one
|
|
556
|
+
// keeps one, in its hero, else its first slot: one of the job's own
|
|
557
|
+
// photos where it has any (AGL-3988), else a neutral starter.
|
|
447
558
|
if (input.source && !photos.some((photo)=>photo == null ? void 0 : photo.src)) {
|
|
448
559
|
const at = Math.max(0, slots.findIndex((slot)=>slot.role === 'hero'));
|
|
560
|
+
const lead = _extends({}, slots[at], {
|
|
561
|
+
product: undefined,
|
|
562
|
+
people: undefined
|
|
563
|
+
});
|
|
564
|
+
const [pick] = aiLayoutJobPhotoPicks([
|
|
565
|
+
lead
|
|
566
|
+
], [
|
|
567
|
+
true
|
|
568
|
+
], own, [
|
|
569
|
+
null
|
|
570
|
+
], input.seed);
|
|
449
571
|
const [floor] = rotate(NEUTRAL_STARTERS, aiLayoutSeedNumber(input.seed));
|
|
450
|
-
const
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
572
|
+
const starter = floor ? AI_LAYOUT_STARTER_PHOTOS[floor] : null;
|
|
573
|
+
if (pick) photos[at] = pick;
|
|
574
|
+
else if (starter) photos[at] = said({
|
|
575
|
+
src: starter.src,
|
|
576
|
+
width: starter.width,
|
|
577
|
+
height: starter.height,
|
|
578
|
+
alt: starter.alt
|
|
456
579
|
});
|
|
457
580
|
}
|
|
458
581
|
return aiLayoutDropEmptyPictures(aiLayoutPlacePictures(nodes, slots, photos), slots, photos, input.sectionIds);
|